• Please review our updated Terms and Rules here

CGA 160x100x16 bidirectional scrolling

I'm pretty satisfied with the current state of the library. So the final piece was to, ugh, document it.Certainly not my strong point, but hopefully gives enough information to begin discussion and work on games. Excerpt from the updated README:

"Creating fast graphics routines using this low resolution (lores) mode presents a real challenge, especially when coupled with a 4.77 MHz 8088. The goal of this project is to build a library of routines and tools to help write games that can take advantage of this under-utilized graphics mode."

Read the entire file here: https://github.com/dschmenk/LORES

As this is turning into more of a blog, continued updates will be on the GitHub project page.Feel free to join the development progress there.
 
So I had to share the latest development: EGA and VGA support. Although the library is directed towards the CGA, I didn't want to completely exclude the EGA and VGA from participating. I figured out how to set the same 160x100 16 color mode on these adapters. However, the scrolling technique won't work on the EGA/VGA due to the 16K mirroring on the CGA. The API doesn't change, but will use the same routines that exist to redraw the screen every frame instead of doing a scroll and partial redraw. By implementing double buffering to hide the redraw, it will visually look similar, however, it will incur a higher CPU load. Assuming a computer with EGA/VGA will have a higher performance CPU than a 4.77 MHz 8088, it may overcome the additional load. Updated source and binaries on GitHub.
 
The biggest issue with writing a tile map and sprite library is asset creation. Graph paper and hexadecimal arrays are no ways to deal with sprite and tile images. To make asset creation a little easier here are some tools to help. They're pretty minimalistic but useable.

The tile set and tile map editor:

mapedit_000.png

The sprite editor:

sprited_000.png

Documented here: https://github.com/dschmenk/LORES/tree/main/SRC/MAPEDIT

Both MAZERUNR and SPRTDEMO have been converted over to load their assets from files. A newl library API is available for reading and writing the different file formats.
 
Work has continued on the LORES library. Many corner cases have been fixed and to test the functionality, I've written the first level of a top-down 2D scroller:


Code:
                                   RepelZ

 Your region has just been invaded by a neighboring country. You must defend
 your homeland by destroying the invading tanks and surface-to-air (SAM)
 launchers with your drone. They will be advancing on roads leading from the
 region boundary (marked in red). Don't go past the red boundary as the enemy
 has full control of the airways and your drone will be immediately destroyed.
 The SAM installations will launch missiles at your drone when in range. The
 faster you fly the drone, the harder it will be for the SAM to hit, but also
 more difficult to fire at the tanks and SAM launchers.

                    Controls:
                        UP ARROW    - speed up
                        DOWN ARROW  - slow down
                        LEFT ARROW  - turn left
                        RIGHT ARROW - turn right
                        SPACEBAR    - fire rocket
                        ESCAPE      - quit game

Good Luck!

     *Any similarity to current events may or may not be coincidental.

So this is very much a work-in-progress and I'm looking for feedback. Included in the ZIP file are two versions: normal and profiling (using the border color method). If anyone has a bone stock IBM-PC with a CGA that snows, I'd really like to know if it snows during gameplay. The map is a 64x64 tiled world. A pretty small map as the LORES library should be able to handle one four times this size. However, it takes quite a bit of effort to populate even a 64x64 map. Still working on that. Other gameplay interactions like SAM range, speeds, sounds, input controls, etc. are still preliminary.

Let me know what you think...
 

Attachments

  • REPELZ.zip
    41.1 KB · Views: 6
Pretty cool. I'm not sure about the main sprite though. Is that supposed to be a TB2? I think it might look smoother if you get rid of the 22.5 degree directions.
 
Pretty cool. I'm not sure about the main sprite though. Is that supposed to be a TB2? I think it might look smoother if you get rid of the 22.5 degree directions.
That's exactly the kind of feedback I was looking for. I am also torn on the 16th angles. The requirement to scroll horizontally by two pixels looks odd for non 90 and 45 degree scrolling. But I thought I'd let others chime in, and now I know what to do next.
 
Very nicely done! Gave it a go on my 5160 with an IBM CGA card and there's zero snow during the actual game.
(It snows during the black screen just before the game starts, but at least that can be easily hidden by blanking out the video.)

Curiously, in PCem I do see the snow... it starts about 1/3 of the way down the screen. The profiling version turns the overscan red right about then (I assume this marks where you're writing to video RAM - on the real hardware, it goes red only during vertical blanking, just below the bottom of the active raster). PCem typically isn't *that* far off in terms of timing, so I gotta wonder how you're scheduling that interrupt...
 
Thank you for taking the time to try it out!
Very nicely done! Gave it a go on my 5160 with an IBM CGA card and there's zero snow during the actual game.
(It snows during the black screen just before the game starts, but at least that can be easily hidden by blanking out the video.)

Interesting. This is probably where it draws the initial view, but I thought I had blanked the video out during the drawing (thus the black screen). I'll have to revisit that and hopefully see what I missed.
Curiously, in PCem I do see the snow... it starts about 1/3 of the way down the screen. The profiling version turns the overscan red right about then (I assume this marks where you're writing to video RAM - on the real hardware, it goes red only during vertical blanking, just below the bottom of the active raster). PCem typically isn't *that* far off in terms of timing, so I gotta wonder how you're scheduling that interrupt...
That is odd. PCem could be delaying when the PIT starts counting? I trigger the PIT right after counting the active scanlines during library initialization. DOSBox gets it right. The overscan is set to red after the waiting for the PIT interrupt and reset to brown (in repelz's case) when all the critical drawing is complete. There will be some jitter every third frame when the interrupt handler reflects the interrupt to the original INT8 handler in an attempt to keep the system timing semi-accurate.

This gives me a few things to work on but great relief that the critical code is working as intended.
 
Hm, come to think of it, I remember seeing something weird with PCem's PIC implementation. I could be wrong, but my suspicion was it failed to set the correct bit to indicate that a given IRQ is in service. *If* that's the problem, and another IRQ is firing while your interrupt handler is executing, it can throw off the timing (or worse). Just a guess, because DOSBox didn't have that issue. But at least it's fine on the real hardware.

Keeping the system clock in sync is annoying - I guess you can always count ticks while you're handling INT 8, then when you're done divide by the frequency ratio, and update the BIOS counter yourself. But then you have to mind the midnight turnover, and so on... 😐
 
I've tried to address some of the issues in the attached update. By default, only 90 and 45 degree angles are used for the drone direction. It does clean up some of the jerkiness with the 16th angles, but makes aiming the drone a little harder. You can get the previous functionality by adding a " -16" parameter to REPELZ. Also worked on keyboard responsiveness. Not too responsive, but not too draggy. At least I think so. The SAM launchers are also much harder to evade, but fewer of them. However, I suck at games - even the ones I write, so others may find it easy. Work continues on filling out the map. Really need some sort of algorithmic map generator.

As for PCem, I think there is some discrepancy on how the PIT latches new count values. My understanding from reading the data sheet (and confirmed with hardware and DOSBox) is that the new count value is immediately latched and written into the current count. I've seen other documentation claim that the new count isn't latched until the current count reaches zero. This could explain why PCem doesn't generate the PIT interrupt at the last active scanline, when I write the count value to synchronize the video and timer, instead waiting until the current 18.2 Hz count reaches zero to latch the new count and screwing up my synchronization. That's my theory, anyway.
 

Attachments

  • REPELZ.zip
    41.9 KB · Views: 3
Hmm I see what you mean about the aiming. Zone 66 also has 22.5 degree movement but it doesn't look jerky. I think the jerkiness might have to do with how you are doing the scrolling. I slowed it way down and it seems like there is no diagonal scroll for the 22.5 angles. Instead of something like over 2, up 1, can you do over 1, diagonal 1?

Also would it be possible to make it so discrete keypresses turn the drone one step? I think that would make it feel more responsive and easier to aim.
 
Hmm I see what you mean about the aiming. Zone 66 also has 22.5 degree movement but it doesn't look jerky. I think the jerkiness might have to do with how you are doing the scrolling. I slowed it way down and it seems like there is no diagonal scroll for the 22.5 angles. Instead of something like over 2, up 1, can you do over 1, diagonal 1?
I'll need to re-think how I'm moving the drone around. I simply used 16.16 fixed point numbers just to make everything easy, but still had to hack the diagonals so they moved cleanly. Back to the drawing board.

Also would it be possible to make it so discrete keypresses turn the drone one step? I think that would make it feel more responsive and easier to aim.
That is actually how I started out and is still a build option. What I didn't care for is by quickly pressing the keys made it spin around like a top. Very un-airplane like. Controlling the rate of turn is needed to make it somewhat realistic, which is why I implemented the hold-key-down-to-keep-turning approach that I like better if there are more directions the drone can turn.

Thanks again for the feedback, much appreciated

I don't think I posted a link to the source and other binaries. The binaries I included in the ZIP files above are actually the Borland compiled ones because it has the interrupt function type I used for the keyboard handling. They are named REPELZBC.EXE and REPELZPBC.EXE in the repository. The other REPELZ.EXE and REPELZP.EXE are the MSC compiled versions and still use the single keypress to turn code (because no easy interrupt routines in MSC 5.1) so you can check that out.

 
As for PCem, I think there is some discrepancy on how the PIT latches new count values. My understanding from reading the data sheet (and confirmed with hardware and DOSBox) is that the new count value is immediately latched and written into the current count. I've seen other documentation claim that the new count isn't latched until the current count reaches zero. This could explain why PCem doesn't generate the PIT interrupt at the last active scanline, when I write the count value to synchronize the video and timer, instead waiting until the current 18.2 Hz count reaches zero to latch the new count and screwing up my synchronization. That's my theory, anyway.

As far as I know, it's possible to latch a new counter value so the PIT doesn't load it until the current one hits zero. But this only works if:

* You're using timer mode 2 or 3, and
* You write your value without sending a new command byte first.

Unless both of the above are true, the new counter value takes effect immediately. (See also: https://scalibq.wordpress.com/2015/09/13/latch-onto-this-its-all-relative/)
If that's not the case with your code, maybe PCem is too liberal in allowing values to be latched until the next terminal count. I've been bitten by such inaccuracies before, so just a little caveat for anyone who uses emulators to speed up testing/development, like I mostly do...
 
As far as I know, it's possible to latch a new counter value so the PIT doesn't load it until the current one hits zero. But this only works if:

* You're using timer mode 2 or 3, and
* You write your value without sending a new command byte first.

Unless both of the above are true, the new counter value takes effect immediately. (See also: https://scalibq.wordpress.com/2015/09/13/latch-onto-this-its-all-relative/)
If that's not the case with your code, maybe PCem is too liberal in allowing values to be latched until the next terminal count. I've been bitten by such inaccuracies before, so just a little caveat for anyone who uses emulators to speed up testing/development, like I mostly do...
Interesting information. This is the sequence I use to set up the PIT, which I believe I pilfered from one of your websites:

Code:
        mov     al, 036h                ; Program PIT to count 19912
        out     043h, al                ; Timer 0, LSB, MSB, mode 3
        mov     al, 0C8h                ; 59.94 Hz
        out     040h, al
        mov     al, 04Dh
        out     040h, al

So yeah, caveat emptor (as I don't know how to translate "emulator beware" into Latin).
 
After taking into consideration all the great feedback, here is a version that uses a combination of fixed point and a Bresenham style DDA to improve the diagonal scrolling and movement. Reverting back to all 16 directions, this feels and looks much better. Hope you agree. Again, feedback always appreciated.
 

Attachments

  • REPELZ.zip
    42.2 KB · Views: 5
It's definitely improved. 22.5 movement still seems a little jagged but maybe it's just the low resolution. The only other thing I can think of is to skip frames to eliminate the scroll steps that are purely horizontal or vertical. But that might make the scrolling look too choppy.
 
The two-pixel limitation is going to make it a little jerky. Unfortunately there is a limit to the amount of pixels that can be updated during scrolling, two pixels horizontally and two pixels vertically is about as much as can be updated during inactive video. I think this will be the best odd angles are going to be. Developing different game styles are going to reveal the strengths and weaknesses of this library. Hopefully the fast frame rate makes up for some of the deficits.

I'm about ready to wrap this exercise up and move to another, perhaps a side/vertical scroller. Any ideas?
 
Last edited:
A little blurb on some of the implementation choices for RepelZ for anyone interested:

RepelZ - How It's Done
How to create a full-screen, 16 color, 60 FPS game on a 4.77 MHz IBM PC with CGA video. First, the LORES library is used to provide the high performance 2D scrolling and sprite functionality. This supports a simple to use API that does all the heavy lifting, leaving just the game logic to implement.

Sprite Usage
The PC can't push a lot of pixels in the short time allotted during inactive video to avoid CGA snow and video tearing. The sprites must be of moderate size to update everything on the screen. Keeping the sprite size from around 8x8 to 12x12 pixels will be necessary to have a few of them being simultaneously active on the screen. With a 160x100 resolution screen these are bigger than what would be available on a 320x200 or 640x480 resolution screen, so more appropriate than might be initially assumed.

Scrolling Algorithm
One of the hardware limitations of the LORES library is the horizontal two-pixel scrolling increment. In order to create a consistent appearance during diagonal scrolling, vertical increments can be set to two-pixel increments as well. By moving the center around by even pixel coordinates locked to the main sprite, it will be harder to visually recognize that the scrolling is actually limited to two-pixel amounts. The movement is controlled with a combination of a 16.16 fixed point move rate and a Bresenham algorithm to choose the pixel coordinates. Other sprites are moved by a simple 16.16 position and increment.

Mini-Scheduler
The lowly 4.77 MHz 8088 struggles to get much processing done 60 times a second. Luckily, game logic doesn't all have to be run at 60 Hz, much high level logic can be run at a slower rate. In order to get all aspects of the game logic to execute in a timely fashion, different parts of the game logic are scheduled based on framerate. Game logic is broken up into 4 tasks that are run every fourth frame. The LORES library provides a frame counter variable that increments every frame. This can be used to schedule the different tasks such as input handling, sound generation, enemy AI, and such.

Efficient Input
The input method installs a keyboard IRQ handler to monitor key presses and releases. The key state can be quickly checked with very little overhead compared to other input methods that use BIOS calls which can have much higher overhead.
 
And now for something completely different. Well, not completely. Here is a test of just the sprite functionality during gameplay requiring a slightly different approach to get 8 sprites to appear simultaneously moving on the screen. Scrolling is only done during the opening and closing scenes:

Invaders from Outer Space​



Earth is being invaded by space aliens. You are Earth's last defense. Based on the Moon, you must shoot the aliens before they get down to you and destroy your ship. Otherwise, Earth is lost.

Controls:
LEFT ARROW - move left
RIGHT ARROW - move right
SPACEBAR - fire missile
ESCAPE - quit game

Implementation Details​

Unlike the other testbed programs, "Invaders" only uses scrolling during the opening and closing scenes. Actual gameplay relies just on sprites to render the play field. There are a possible 8 active sprites: 1 ship, 1 missile, and 6 aliens. A stock IBM PC cannot update all 8 sprites 60 times a second without CGA snow or image anomalies so alien movement is interleaved, one alien per frame. It is hard to recognize that the aliens aren't moving in sync.

The LORES library won't clip sprites to the map boundaries, but it will clip them to screen boundaries (an implementation performance decision). In order to get clipped sprites, "Invaders" creates a tile boundary surrounding the playfield. The view is set inside this boundary so the sprites will be nicely clipped to the screen edges.

The background was created from a NASA "Earthrise" picture. The slicer.c program in the MAPEDIT directory is run on a modern computer and will convert the NetPBM RGB image into either a dithered or a closest match 4 BPP IRGB version. It will then slice the image in 16x16 pixel tiles, reducing duplicate tiles, and saving it in a format to be loaded by the LORES library (or further edited with MAPEDIT).

As always, source can be found here: https://github.com/dschmenk/LORES/tree/main/SRC/INVADERS
 

Attachments

  • INVADERS.zip
    32.3 KB · Views: 4
...
(It snows during the black screen just before the game starts, but at least that can be easily hidden by blanking out the video.)

...
Going over outstanding issues, I see where I enabled video before filling the screen with ASCII character 221. Fixed and I hope this clears up the snow during mode set. All binaries are updated on GitHub but I've also included INVADERS with the fix as well as an improved missile/alien hit test (not so hard to kill those suckers now).
 

Attachments

  • INVADERS.zip
    32.3 KB · Views: 1
Back
Top