• Please review our updated Terms and Rules here

CGA 160x100x16 bidirectional scrolling



isotest.png

I've always been a fan of the isometric view for games. It is a visually interesting projection that combines the simplicity of a top down view with a pseudo-3D aesthetic that conveys a sophisticated image without the overhead of a real 3D implementation. I figured that the LORES library might be able to pull off a simplified isometric environment, but there are a few issues to work through in order to do so.

One of the constraints of the LORES library scrolling actually works in our favor: scrolling horizontally by two pixels with the ability of scrolling vertically by one pixel matches well with the 2:1 aspect ration of an isometric tile. And that is about the only common ground between the LORES library and isometric tiles.

The LORES library is based around square 16x16 pixel tiles. Isometric tiles (technically, these aren't perfect isometric tiles) look more like flattened diamond shapes with a 2:1 aspect ratio - two pixels wide for each vertical pixel. This gives the impression of looking at a scene from a 45 degree diagonal and an elevated height. I took the approach of mapping 32x16 isometric tiles to 16x16 square tiles using a combination of modern tools and my image converter program, 'slicer', found in the MAPEDIT directory. Note that it isn't as simple as just making tiles that are twice as wide as the 16x16 LORES tiles - the isometric tiles' origin alternates from intersecting the 16x16 tile grid to halfway between the LORES tile grid. Read on to see my conversion workflow...

Creating an Isometric Grid and Map​

Watching some YouTube videos on how all the cool kids create their isometric tile maps for use in Steam, I found a very nifty program called Aseprite: https://www.aseprite.org. I went ahead and bought this tool, affordably priced at $20 for a LOT of functionality. I created my first test map using Aseprite, but realized that I could have done everything just as easily using GIMP: https://www.gimp.org. So I exported the map file over to GIMP and worked from there.

The Isometric Grid​

First, I created an isometric grid to align the 32x16 isometric tile to the 16x16 square tiles and create a guide for drawing the isometric tiles. Image layers are a powerful tool used in many image manipulation programs like Photoshop, Aseprite and GIMP. Taking advantage of this tool is very useful. I created the base layer as the isometric grid. Drawing the actual isometric tile images on transparent layers above this allows for easy edge alignment. Use as many layers as you want for floors, walls, etc. By keeping them on separate layers as you draw them makes arrangement a breeze. I also made a solid black layer just above the grid layer that I normally keep invisible so the grid shows through. By toggling its visibility, it shows me what the map looks like without the underlying grid.

Isometric Tiles​

Using layers for different tiles makes for an easy alignment process. For instance, create a floor tile that will be used many times. I will create the base tile off to the side. Once satisfied, I will select it and copy it. Then the copy can be pasted around the map using the grid as a guide. You will find my 'isotest.xcf' GIMP image file that has all the isometric tiles flattened to one layer.

Exporting Map for Slicing and Conversion​

When it's time to convert the isometric map into something digestible to LORES, set the visibility of the black background layer (or whatever color you want for the background) to 'visible' and export to a '.pnm' Portbale BitMap format using 'Raw' data values. This will be fed into the 'slicer' tool. The slicer tool is very generic C code that should compile under just about any modern platform. It takes a Portable BitMap '.pnm' file (raw RGB values with a simple ASCII header) and converts it into a '.SET' tile set file and a '.MAP' tile map file ready to import using the LORES MAPIO functions or further editing with MAPEDIT. 'slicer' will reduce all identical tiles into one to save memory so it is important to carefully align the isometric tiles and not introduce any spurious pixels that will cause additional tiles to be generated. My command line to for conversion is:

../MAPEDIT/slicer -g 1.8 -n isotest

This uses a gamma of 1.8 and disabled dithering for a closest 4 BPP IRGB color match, creating 'isotest.set' and 'isotest.map' from 'isotest.pnm'.

ISOTEST.EXE​

Finally there is a test harness program to scroll around the map. It simply uses the four arrow keys to scroll along the isometric axes, quitting with the 'ESC' key. It includes two additional routines to convert between world coordinates (s,t) and screen coordinates (x,y).

Limitations​

Of course there are going to be limitations to what can be accomplished with the LORES library and what one might expect from a modern isometric implementation. Most isometric games allow characters to move behind objects and be properly occluded. LORES has no such concept, so care must be taken to keep objects/sprites always in front or hide them completely when occluded. Adding height to the tiles isn't impossible, but will take additional programming effort over what I've done here. Remeber there is a limit to the number of sprites that can be updated per frame, so don't expect to be able to implement melee of fighting sprites without clever programming & scheduling.

Future Effects​

Adding visibility to the map is one thing I would like to explore. Populating the map as the player moves through the map so the entire map isn't exposed all at once should be quite doable.

Asset files and executable can be found here: https://github.com/dschmenk/LORES/tree/main/SRC/ISOTEST
 

2D Platformer Test​


The most obvious use of the CGA LORES library is to create a 2D side view platformer style game. This is a simple Lode Runner looking test to try out the new sprite page slicer:
Code:
            Controls:
                LEFT ARROW  - run left
                RIGHT ARROW - run right
                UP ARROW    - ascend ladder
                DOWN ARROW  - descend ladder

Implementation Details​

I discovered that there is a vibrant community building Game Boy games. The Game Boy has a graphics resolution of 160x144 which maps pretty well to the 160x100 CGA LORES resolution, at least as far as assets go. Even the tile size of 16x16 matches the LORES tile size. So there are potentially lots of available assets from the Game Boy development community. Look at Game Boy Studio for many ideas and great pixel artwork.

The running/climbing/falling man sprite is derived from the Game Boy Kung Fu Man available here: https://chasersgaming.itch.io/rpg-asset-character-kung-fu-man-gameboy. This is also a great site for assets. The PICO-8 is a fantasy console that has a community of asset creators that also matches well to the LORES library.

Once again, the limitations of a 4.77 MHz CPU raises its ugly head. Even though this sprite doesn't look very big, it really pushes the limits of the number of pixels that can be updated during inactive video. It is a 14x23 pixel sprite which is only 294 pixels, but that pushes our poor 8088 to its limits. If scrolling and other sprites are going to be updated, care will have to be taken to ensure timing doesn't cause sparkles or tearing.

However, I think it looks great.

As always, source and binaries are available here: https://github.com/dschmenk/LORES/tree/main/SRC/PLATFORM
 
It doesn't sound highly optimized if a single sprite 14x23 is pushing a 8088 to it's limited in LOW-RES. My Amstrad runs at 4Mhz, there's no hardware sprites with a Z80A processor (same as Gameboy), LOW-RES at 160x200. Are you drawing the Sprite from Pixel or using Bytes to Mask the Sprite?
 
The 8088 only gets a small fraction of the total frame time to push all this data out, if you want to avoid display issues. That's a limitation of CGA architecture in this particular mode. I don't think the Amstrad CPC or Gameboy have similar constraints (could be wrong of course).

The 'safe' limit is probably around twice the size of that sprite, but on top of that it also has to scroll... so it's a tight squeeze.
 
It's sometimes hard to remember just how slow a 4.77 MHz 8088 is. It really is about the same performance as its contemporary 8 bit CPUs. In contrast, my 7.16 MHz Compaq Deskpro with a NEC V30 can run the entire game loop during inactive video with time to spare. But since I'm targeting the original IBM PC (and Compaq Portable)...

In this case, the size of the sprite is only half the story. In order to update the sprite without an erase/redraw cycle, I pre-render the sprite with the background that overlaps the previous sprite position into a memory buffer and use one draw operation to update the screen. So the effective size up to 50% more than the actual sprite (I had to increase the overlap border size for this test) and the draw operation is really nothing more than unrolled memory move instructions. As VileR mentioned, updating the screen memory has to occur during inactive video - there is no double buffering and you can't really race the beam because accessing video memory during active video will case sparkle artifacts (snow).

The consolation is that you have the opportunity to update the screen 60 times a second, so you don't have to do everything at once. The trick is to schedule what gets updated and when, giving the impression of fluid animation. Makes me feel like the Atari 2600 guys had it easy ;-)
 

2D Side Scroller Test​

The above platform test now with scrolling!


If anyone wants to test this out on a 4.77 MHz 8088, I'd appreciate the feedback. Vertical or horizontal scrolling should make timing, but the cases where our hero jumps and it scrolls diagonally may push the envelope. There *may* be snow. It looks fine on my Compaq Portable, but it doesn't suffer from snow. The profiling bars creep in to active video when this happens, so I'm not hopeful.

This version is actually updating the sprite every other frame, leaving time for additional sprites/game logic to run during the opposite frame.
 

Attachments

  • SideScroller.zip
    63 KB · Views: 4
I don't have real hardware setup to test on but it looks great in dosbox. What are the differences between the EXEs? Also is there a key to jump?

How many enemies can you add at 4.77 MHz?
 
I don't have real hardware setup to test on but it looks great in dosbox. What are the differences between the EXEs? Also is there a key to jump?

How many enemies can you add at 4.77 MHz?
Thanks for trying it out.

The four binaries are the four build options: Microsoft C 5.1 or Borland C++ 3.1 with and without profiling bars (border color). You will only see the profiling bars on a real CGA, so useless for emulation or EGA/VGA. So:

PLATFORM.EXE = MSC no profiling
PLATFRMP.EXE = MSC w/ profiling
PLATBC.EXE = BC31 no profiling
PLATPBC.EXE = BC31 w/ profiling

Now, I didn't add a jump key for a few reasons, but mostly this was just a test to see if developing this test into a real game would be worth it. It wouldn't be hard, but I'd have to add more art for the hero sprite.

Have I covered the bases for the different styles of scrolling games? Is there something else I should provide a demo/test for? Does anyone really care?
 
How many enemies can you add at 4.77 MHz?
Forgot to answer this part of your question. And it's a good one. I should say "That is left as an exercise to the reader" or something along those lines ;-) Of course the correct answer is "It depends".

If you look back at the other demos, you will notice a few "cheats" to have enough enemies on screen to make it interesting. In RepelZ, the enemy tanks and SAM installations are actually tiles, not sprites. Looking at it as a cost analysis: tiles are cheap, sprites are expensive. When enemies are destroyed, an explosion sprite overlays the enemy tile as it gets updated to a tile of a destroyed enemy. The biggest issue of using tiles over sprites is that they are mostly non-moving and static. RepelZ does use sprites for the drone and missiles but they are small and can be updated along with scrolling the map.

With Invaders, there are many sprites on-screen at a time that appear to be moving all at once, but they are actually updated in a round-robin fashion, one per video frame so as to not overload the CPU with inactive-video updates. However, Invaders doesn't scroll during gameplay so the only updates are the sprites.

With PLATFORM, I'm trying to push the boundaries with a large animated hero sprite and simultaneous scrolling. If the enemies and other objects can be implemented with tiles, then there is a minimal impact to rendering overhead. If there are going to be sprites on-screen with the hero during scrolling it then gets more interesting. The LORES library has to keep track of updates to sprites (and tiles that change) every frame. It does intersection checks between sprites, changed tiles, and screen boundary visibility during scrolling. Even if a sprite doesn't explicitly get updated in the game loop, it will be flagged for redraw should it hit any of the intersection tests. The implication is that if our hero sprite moves and the screen scrolls causing another sprite to become visible, our poor 4.77 MHz 8088 may fail to get all the drawing done during inactive video.

That may have been more of an explanation than you wanted. Hope that helps answer some of the challenges, though. Anyone want to try their hand at writing a LORES game/demo?
 
Last edited:
Jeez, doesn't anyone read anymore?
I was unable to find in your source anything remotely resembling the port access for this to be hardware driven, but if as you say it is present, it's then probably the fact that shithub and I get along like sodium and water. Not a fan as I find it ridiculously and painfully cryptic, aggravating, and in general a total waste of everyone's time.

Which is why now that I just told it to "for **** sake just hand me a zip" I have a much better idea what your code is doing. My attitude these days is **** git and the source it rode in on.

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.
When I was writing Paku Paku I got increasingly annoyed with the people who claimed there was enough CPU time to write to the screen without snow at 4.77mhz in something that's an actual game. THERE ISN'T!

One thing I did notice though was that at this absurdly low resolution you don't actually need to maintain the 60hz frame rate. As you said logic can be run a lot slower... so can screen updates!
Generally because the resolution is so low, you only need to update the screen at 20hz and it's still fine with no jitter. Paku Paku does this based off a 120hz (240hz for PC speaker arpeggio) timer that exists to keep the audio updates correct. 120 being a magic number since it divides evenly by 20, 24, and 30, the three different frame-rates at which Paku Paku's levels run.

People wonder why I did that and it's because you always make your timer for what ever you need to run fastest first, then you worry about the smaller stuff. The human ear gets pissy at anything more than 9ms updates in tone or volume. Just as if there's more than 50ms of lag between an action and the event people notice it.

Thus if I were doing this I'd probably aim for 20hz which at 160x100 seems overkill.
I need to backread the thread more, but have you tried reducing the render time to 156x96 to hide the edge draws? Also it kind-of sucks, but it might be more efficient to have your tiles -- for background at least -- only be two pixels wide irregardless of height. Coded sprites would likely also give performance a real boot in the patoot.
If you have the RAM. Which is ALWAYS the fun part of projects like this. I mean if your map tiles were always 2x12 you could optimize their blitting in the non-render area... though that means a full-screen section of map would end up anywhere from 1.3 to 1.5k. Not unmanageable, but could add up fast depending on the size of the completed map.
 
Welcome back, deathshadow.

Yes, a lot has developed from those early posts and worth reading to answer your questions.

To view the current code, it is going to be on GitHub - and I include the link to take you directly to the code for my relevant posts. I get you don't like GitHub, I'm not claiming it is the be all and end all, but it is fairly easy for anyone who has used a computer in the last 25 years to navigate. And you can even download a ZIP file of the whole thing if that is the preferred method of interaction:

Screen Shot 2022-07-15 at 9.25.18 AM.png

Your write-up of Paku Paku is very interesting. The audio processing appears to be quite sophisticated and where a lot of the effort was spent. You pulled off a nice recreation of a classic. However, by supporting the 160x100 16 color mode without dealing with CGA snow it missed the mark. Given that the game is of pretty low complexity there shouldn't be any reason for noisy graphics on a 4.77 MHz 8088.

This brings up other questions:

Did you ever attempt to write snow-free graphics routines?
Have you profiled your code?

I get the impression that your thinking is very traditional when it comes to graphics. The whole purpose of this thread is to approach this graphics mode from a non-traditional direction and build a methodology to profile code in order to meet timing on a 4.77 MHz 8088. If you apply the same requirements you placed on your audio processing to your graphics processing you may find snow-free graphics possible.

Now don't take my critique to mean I'm bashing Paku Paku or your abilities. I understand you're not a professional game developer and did this for the love of it, not a paycheck. But for the same reason, you shouldn't be so adamant about what can and can't be done. There's more than one way to skin a cat.
 
Well, real life has been keeping me away from my hobbies. So I've shucked my responsibilities and fired up the old iron! I was going to do a write-up of my escapades but decided to go with a video, being lazy and with the visual nature of the subject.

Profiling the CGA LORES Library (aka, the cure for insomnia):

 
Interesting discussion here! I've been creating a 160x100x16 driver + graphics for Rick Dangerous 2, the levels of which I've documented on my YouTube channel. Unfortunately it doesn't run too well on an original 4.77MHz XT (even not on a NuXT, which should be slightly faster since it uses SRAM so it lacks DRAM refresh cycles) with an ATi All Wonder Graphics Solution CGA card, but that's partially due to the code being horrible and very unoptimized for the 8088 (the "normal" CGA driver for 4 colour graphics is also pretty slow). I'd be interested though in knowing what ways you guys are perfoming full screen updates - currently I copy the memory buffer with a partially unrolled loop (500 times x 16 times "inc di / movsb" - there's some speed to be gained by only copying the 128 pixels used in-game, haven't touched that yet).

 
Interesting discussion here! I've been creating a 160x100x16 driver + graphics for Rick Dangerous 2, the levels of which I've documented on my YouTube channel. Unfortunately it doesn't run too well on an original 4.77MHz XT (even not on a NuXT, which should be slightly faster since it uses SRAM so it lacks DRAM refresh cycles) with an ATi All Wonder Graphics Solution CGA card, but that's partially due to the code being horrible and very unoptimized for the 8088 (the "normal" CGA driver for 4 colour graphics is also pretty slow). I'd be interested though in knowing what ways you guys are perfoming full screen updates - currently I copy the memory buffer with a partially unrolled loop (500 times x 16 times "inc di / movsb" - there's some speed to be gained by only copying the 128 pixels used in-game, haven't touched that yet).

That looks really amazing. Great artwork. The challenge in copying the whole memory buffer is that it's just too much data for an 8088 - even a 286 - to do in a timely fashion. Slow framerate, screen tearing and snow will result. But 128 pixels per update shouldn't be a problem. When it comes time to scroll, you could take advantage of the same idea used here: only updating newly visible and changed pixels. The score bar at the top of the screen would require some creativity. Keep us posted!
 
That looks really amazing. Great artwork. The challenge in copying the whole memory buffer is that it's just too much data for an 8088 - even a 286 - to do in a timely fashion. Slow framerate, screen tearing and snow will result. But 128 pixels per update shouldn't be a problem. When it comes time to scroll, you could take advantage of the same idea used here: only updating newly visible and changed pixels. The score bar at the top of the screen would require some creativity. Keep us posted!
Thanks! It took my quite a while to draw all tiles and sprites for all five levels, as well as the title screen. And I'm no pixel artist 😄. Frame rate is slow indeed, screen tearing is not that bad, and snow, well, only on original CGA cards that's a problem (the ATi card I'm using doesn't have snow). As for updating only changed pixels, that's a bit of a problem, as a) I'm bound to the technical implementation of the game and b) it's impossible to easily determine what pixels have changed and c) when scrolling almost all pixels will change anyway. But I'm just now starting to profile the code and see where I can cut some corners, as obviously on DOSbox it runs smooth enough.
 
I noticed long ago that certain games seemed to update the background less often than the character and foreground

Made the background seem choppy but things stayed playable
 
How did you do that? The source code of Rick Dangerous 2 isn't available, right?
 
I noticed long ago that certain games seemed to update the background less often than the character and foreground

Made the background seem choppy but things stayed playable
Yeah, there's not a lot scrolling CGA games that also work fine on an original PC or XT. Games like Testdrive needed at least 10MHz to be playable.

How did you do that? The source code of Rick Dangerous 2 isn't available, right?
It isn't, but I reverse engineered it a while ago, and also made a Windows version in C++/SDL. Reverse engineering revealed that the game has loadable graphics drivers for CGA and EGA/VGA (and an originally planned Tandy driver which never saw the light of day), so I "just" made another driver for CGA 160x100x16, and created tiles and sprites for all five levels. Took me a while 😄.
 
I wonder how feasible it would be to implement an AGI interpreter that would run the earlier Sierra adventure games in this mode. At 160x200, those games had twice the vertical resolution of 160x100x16 CGA, but maybe they'd still be playable.
 
As an experiment, I took a screenshot from the original King's Quest I from the web, scaled it down with Gimp to half the vertical resolution with interpolation set to "None", and then scaled it back up to its original resolution, again with interpolation set to "None". It looks like the result gives a fair representation of what a Sierra AGI game would look like when rendered in the 160x100x16 CGA mode. Not as bad as I expected, actually.

The originals on the left and the simulated half vertical res results on the fright:

KQ1 - Castle.png KQ1 - Castle - half vertical resolution.png
lsl1.gif lsl1_half_vert_res.gif

Although a solution would have to be found for text rendering. I guess the game could rapidly switch into text mode whenever the user started to enter a command, or whenever text from the game would have to be presented to the user. Not unlike those framed texts on a black background that would be shown to the audience in those old black-and-white silent movies. AGI games already switch into text mode whenever the user accesses the playable character's inventory.
 

Attachments

  • KQ1 - Castle - half vertical resolution.png
    KQ1 - Castle - half vertical resolution.png
    7.4 KB · Views: 3
Last edited:
Back
Top