dev, computing, games

📅October 10th, 2026

I submitted this entry to GB Pixel Art Jam 2026.

It's a reference to the Cavendish card from the game Balatro:

This pixel art jam is non competitive but your image has to conform to some conform to Game Boy video constraints.

In other words, it has to be 160x144 pixels, 2bit color using a palette of the shades of green #e0f8cf #86c06c #306850 #071821. As an additional restriction for the event it needs to use no more than 192 unique 8x8 tiles.

The image also has to follow the theme for the art jam. The theme gets announced right at the start of the jam to help prevent people from starting early. Everyone's supposed to be allowed the same amount of time. The theme was "3". However you want to interpret the number 3.

At the end of the jam, all eligible entries will be compiled into a slideshow together.

The art jam runs over the weekend. I drew and submitted it late on Friday, since I had some other schedule commitments preventing me from working on it over the weekend.

To validate the number of tiles used, I used this tool: https://gb-studio-tile-count.netlify.app/. When I got done with the main cardface part I found there were some tiles left in budget, but not a whole lot, so I added the "X3".

The screen door background being aligned to the tile boundary helps keep things from going over the tile budget.

Feel free to check it out on the submission page! https://haydenkale.itch.io/x3-banana

October 10th, 2026 at 6:43 pm | Comments & Trackbacks (0) | Permalink

📅September 22nd, 2026

This is a followup to this post: https://cml-a.com/content/2026/09/15/contra-iii-ending-rom-hack/

I playtested the hack and so far, so good. Still there's a minor functional thing I didn't like about the game. I don't like how, when you get to the ending screen, it stays there for no time at all then restarts you right back into a new game.

That's suitable for an arcade game, it's not desirable for a console game. At least for games of this generation, I'm used to them simply hanging at the ending screen until you reboot the system. This is on console, so it'd feel more appropriate to do that. Since I already had the debugger open, I took a look at making this change.

Similar to the first change, we're not dealing with easy, findable numerical quantities like "health points" or "experience points".

But in fact, this task of "changing the control flow of the ending" is even harder because we don't even have a given piece of memory like "difficulty level" to breakpoint on. Instead we're dealing with, like, pure control flow.

As a shot in the dark, I used the debugger to break into a random spot as the ending screen was about to fade out and took a gigantic CPU log up until the first identifiable place where the fadeout started.

Despite this being a seemingly short period of time it created a bunch of log files, like 40 of them, 74000 lines each...

It's likely to be a superset of what we need, although I can't even be sure of that.

From the logs, I did quickly find a spot we can hard lock. As in, replace two bytes of code with

BRA $FE

to spin forever. Technically yes this will hang the game at the ending. But then the game doesn't show the nice clouds or crowd animation, it looks very janky since it literally just freezes. The goal really is to have it keep showing the animations.

There are a couple ways to go about this. My hunch is to go kind of black box about this, and just go dumb analysis of the control flow to get a surgical change, don't exhaustively try to reverse engineer the whole transcript.

It's likely that this transcript contains

  • a bunch of game logic
  • a one-time "should we transition the screen" check with a branch
  • a bunch of VBlank handlers
  • things to play sound and advance the animations

The one-time check is the piece that we need to find, and most likely exists. Obviously if it doesn't then we can revisit that. Since it will be a one-time check, we can just scrape the logs for all the PCs that are executed one time.

I made a tool to do this:

https://github.com/clandrew/logtools/blob/main/FindCodeExecutedOnce/FindCodeExecutedOnce.cpp

The repo is here: https://github.com/clandrew/logtools

The tool does exactly what it sounds like, tracks a hit count for each program counter:

struct CodeCoverage
{
    wchar_t PC[6];
    int HitCount;
};
std::vector<CodeCoverage> coverages;
void MarkCodeCoverage(wchar_t pc0, wchar_t pc1, wchar_t pc2, wchar_t pc3, wchar_t pc4, wchar_t pc5)
{
    CodeCoverage* pCoverage = nullptr;

    // ... pCoverage = the one from the list or initialize a new one

    pCoverage->HitCount = pCoverage->HitCount + 1;
}

Then, it reports all instances where the hit count was 1:

    for (size_t i = 0; i < coverages.size(); ++i)
    {
        if (coverages[i].HitCount == 1)
        {
            wprintf(L"Found code this executed once, at PC=%c%c%c%c%c%c.\n",
                coverages[i].PC[0], coverages[i].PC[1], coverages[i].PC[2], coverages[i].PC[3], coverages[i].PC[4], coverages[i].PC[5]);
        }
    }

In theory, this sort of thing, it'd be possible to create a crazy flexible function tool to do it. Ultimately you'll hit a wall with the tool so honestly I think it's better to write code to operate on logs and ROMs in general.

When I ran this tool on the logs, it found, basically, 5 results:

Found code this executed once, at PC=02FCD2.
Found code this executed once, at PC=02FCD5.
Found code this executed once, at PC=02FCD7.
Found code this executed once, at PC=02FCD9.
...(clipped for brevity)...
Found code this executed once, at PC=02FD64.
Found code this executed once, at PC=02FD67.

Found code this executed once, at PC=1DB401.
Found code this executed once, at PC=1DB404.
Found code this executed once, at PC=1DB407.

Found code this executed once, at PC=00BF6E.
Found code this executed once, at PC=00BF71.

Found code this executed once, at PC=04E2E4.	; Ruled out; too late
Found code this executed once, at PC=04E2E7.
Found code this executed once, at PC=04E2E9.
Found code this executed once, at PC=04E2EB.
Found code this executed once, at PC=04E2ED.
Found code this executed once, at PC=04E2EF.

Found code this executed once, at PC=00EA91.	; Ruled out; too late
Found code this executed once, at PC=00EA94.

These represent 5 pieces of code, since the program counters from the same control flow are grouped together. The two ones at the bottom I was able to rule out quickly by hard locking* them and seeing that the fade effect had already started. So it wouldn't be those.

*to insert a hard lock I use the live memory window to ROM and overwrite the desired code bytes with 0x80, 0xFE which is a BRA-to-the-same-place so CPU will get there and spin. Out of habit I avoid using BRK because some emulators react badly from them or crash.

That leaves the first three. The first one was a big long section. As a test, I changed it to get skipped over, as in changing

$02/FCD0 D0 37       BNE $37    [$FD09]   
$02/FCD2 20 0E FD    JSR $FD0E  [$02:FD0E]  

to

$02/FCD0 80 37       BRA $37    [$FD09]      
$02/FCD2 20 0E FD    JSR $FD0E  [$02:FD0E]  ; No longer executed

However, this had no observable effect on the ending. It must be a false positive.

Moving onto the second candidate:

$1D/B3FF 10 08       BPL $08    [$B409]      ; This branch is not-taken once
$1D/B401 A9 00 01    LDA #$0100              ; Executed once
$1D/B404 8D 86 16    STA $1686  [$05:1686]   ; Executed once
$1D/B407 F6 12       INC $12,x  [$00:0912]   ; Executed once    

Based on the control flow and the time at which it happens, this is a good candidate.
So I changed

$1D/B3FF 10 08       BPL $08    [$B409]  

to

$1D/B3FF 80 08       BRA $08    [$B409]      

so that it is always taken.

And voila, that hangs at the ending, with the looping animation, as needed:

Since this stuff can be a bit hairy with unintended side effects I did a couple full playtests of the game to confirm it looks good.

If you want to apply this change yourself, you can use the PAR code 1DB3FF80.

Or if you want this change plus the changes from this earlier post in a patch, you can download them all in a patch here:

https://cml-a.com/content/data/ContraIII.ips

You can apply the patch using a tool such as LunarIPS or your emulator may apply it automatically if it's in a folder with a ROM with the same name. The patch applies to the USA version SNES ROM.

September 22nd, 2026 at 2:41 am | Comments & Trackbacks (0) | Permalink

📅September 19th, 2026

This is a quick project I submitted for a contest. Important to note that I'm not eligible to win the contest because I'm helping run it, but can submit things for fun.

Here's this one:

If you're really perceptive you may notice the resemblance to the Gordie Howe bridge.

The thing with shadertoy is you can't directly import any meshes or textures, everything needs to be specified in a fragment shader in screen space. So there is a bunch of geometry expressed as sort of screenspace fill tests. And the scene is organized to render foreground and background separately.

  • For the bridges, there's a fill test for "does this overlap with the top of a diamond shape". To do that for a diamond shape at the origin, where its width and height are 1, it's simple, you just add X and Y and check that the sum is less than 1. Then you can adjust the formula where the diamond is squished (i.e., X and Y radius are different) and is adjusted to a location that's not the origin.
    • The only other interesting thing is there's some extra logic to add sort of a falloff to the edges so they look smooth and antialiased.
  • For the clouds, there's a similar fill test for "does this overlap with an ellipse". Similarly, the fill test is simple for a circle at the origin of radius 1. And then you can work it out for a squished circle (i.e., an ellipse) where it's at a location that's not the origin.
  • For the stars and plane (the red dot is an airplane), those are just simple aliased filled circles. They are small so it doesn't seem worthwhile to alias them. For some extra visual interest, the stars have kind of a twinkling effect.
  • The plane travels back and forth based on a sine wave of the current time. This seems a bit more visually interesting than seeing a plane always travel in the same direction.
  • The clouds are different colors and travel by taking modulus of time, so they always travel one direction, not back and forth. So do the cars. I think this makes the most sense.

To view this shader in Shadertoy, here it is: https://www.shadertoy.com/view/f3dGDj

September 19th, 2026 at 4:20 am | Comments & Trackbacks (0) | Permalink

📅September 15th, 2026

This is about the game Contra III: The Alien Wars for Super Nintendo.

The game shipped with some weird quirks.

One quirk is that USA version is harder than the Japanese version in a way:

  • In the Japanese version, you get the "true" ending when you beat the game on any difficulty.
  • In the USA version, you get the "true" ending only when you beat the game on Hard.

For casual play / non-competitive purposes, it'd be nice to get the gameplay of the Japanese version but with the text and localization of the USA version.

To do that, use these cheat codes (e.g., in an emulator):

05FD9DB3
1DB0F280
1DBB3C80
1DBBADEA
1DBBAEEA

For info about where these came from, see below.


First I did a quick Internet search to see if someone already had gotten something like this, and nothing showed up. There were codes and things to get increased ammo, lives, things like that. Maybe anyone who cares about this simply plays the Japanese version.

I opened the game in a debugger. Something must store the difficulty setting, and read off of it in order to load the ending.

The problem is, we don't know how it is stored or where. Values like "difficulty" can be more annoying than ordinary numeric values like e.g., health or experience points, because usually you know what numeric quantity health points are, so you can look them up in system memory. Here, I don't know if Easy=0, Normal=1, Hard=2, or some other system.

As a shot in the dark, I opened a live memory window and cycled through the difficulty ratings in the Options. Very quick and dirty approach. I like to mention this because I'm being honest about what worked but also what didn't work. Anyway, for given values I saw them change in a way that corresponded to the current setting:

So, that's encouraging. But when I exited out of the menu, those values in memory got immediately clobbered. So, scratch that. What must be happening is those values get read and written to the "real" place.

For this situation, I could do a break-on-read for the above highlighted areas and try to follow through what the net effect write is, what write takes an effect for gameplay. The thing is, I don't know how complicated that code path is, it could be super simple or not. We don't know. My reflex is to not pull on that piece of string just yet, unless we need to.

So scratch that, and try a different way.

I start the game, once on Easy, once on Normal and once on Hard.

For each, pause immediately upon entering gameplay and take a memory dump:

This produced 3 dump files, I gave them names:

The reason for pausing immediately is to reduce noise and random other processing the game is doing, that scrambles memory. There will still be noise but at least we can minimize it.

Then I did a diff of the 'easy' and 'normal' dumps in a hex editor (I used HxD) and used the 'hard' one to corroborate.

For example, if Easy=A and Normal=B and Hard=B, then it's not a candidate value, because Normal and Hard should be different. There's some baked-in assumptions about things being done in a non-insane way, obviously you can revisit those assumptions if you ever need to.

The diff is noisy, any RAM diff is going to be noisy, but it produced a bunch of candidate values.

After ruling out the first few candidates, I found this:

That's 0x84 in the dump file which corresponds to 0x7E0084 in RAM.

The diff shows that offset contains 0x00 for Easy difficulty, 0x02 for Normal, and 0x04 for Hard. I did a couple tests and it reproduces. So it seems like we have the right value and location. Success.

Something probably reads that value, and uses it to control the 'ending' logic (again, making assumptions about things being done in non-insane ways).

So the next step is to do a break-on-read on that value in memory when you get to the ending. This is assuming that it does the check fairly late and just-in-time, which ended up being true.

After the brain exploding animation finishes and Jimbo is carried away on the helicopter, the breakpoint hits:

From there if you do a pretty straightforward local disassembly you will find this code. I added some comments.

$1D/B0ED A5 84       LDA $84    [$00:0084]   ; Load the difficulty setting
$1D/B0EF C9 04 00    CMP #$0004              ; Is it set to Hard?
$1D/B0F2 F0 0B       BEQ $0B    [$B0FF]      ; If so, go to DifficultyIsHard

DifficultyIsEasyOrNormal:
$1D/B0F4 A9 00 01    LDA #$0100              
$1D/B0F7 95 24       STA $24,x  [$00:0924]  
$1D/B0F9 A9 25 00    LDA #$0025              
$1D/B0FC 95 12       STA $12,x  [$00:0912]   ; Cue up the teaser ending
$1D/B0FE 6B          RTL                     

DifficultyIsHard:
$1D/B0FF F6 12       INC $12,x  [$00:0912]   ; Cue up the final form of the boss
$1D/B101 6B          RTL                     

To get to the final form of the boss, a pretty simple way then is to change

$1D/B0F2 F0 0B       BEQ $0B

to

$1D/B0F2 80 0B       BRA $0B

In other words, use the code

1DB0F280

since PAR codes take the form aaaaaavv where aaaaaaaa=24bit hex address, vv = 8bit hex value.

Great, so we can apply that patch and see it unlocks the final form of the boss:

The only problem is once you beat the boss, if you change nothing else, you'll still get the 'bad' ending.

So there must be subsequent reads of the difficulty value, and those get used to trigger the ending logic.

I set another break-on-read and found those. There were a few.

First, here:

$1D/BAF4 A4 84       LDY $84    [$00:0084]   ; Load the difficulty setting
$1D/BAF6 BE 9B FD    LDX $FD9B,y[$05:FD9D]   ; Load a value from some array, indexed at the difficulty setting. 
					     ; 	In this case,  
					     ;                      for Easy = 0x0, we load 0xFDA1
					     ;                      for Normal = 0x2, we load 0xFDAA
					     ; 		            for Hard = 0x4, we load 0xFDB3
					     ; These are all located at ROM 05:FD9B
$1D/BAF9 22 A8 90 00 JSL $0090A8[$00:90A8]   ; Proceed with the arg loaded.

The value that gets loaded from the array determines the control flow of the ending. E.g., the 'Press Start Key' prompt. It seems very coupled to other uses of the difficulty setting. If you simply change the load from the array to always map to the 'hard' one, it gets further along, showing the 'congrats' screen, but not the full ending. So it is necessary but there's a further change needed.

Setting another break-on-read, there's this:

$1D/BB37 A5 84       LDA $84    [$00:0084]   ; Load the difficulty setting
$1D/BB39 C9 04 00    CMP #$0004              ; Is it set to 'Hard'?
$1D/BB3C F0 07       BEQ $07    [$BB45]      

DifficultyIsEasyOrNormal:
$1D/BB3E A9 29 00    LDA #$0029              
$1D/BB41 95 12       STA $12,x  [$00:0912]   ; Cue up the next part of the ending
$1D/BB43 80 02       BRA $02    [$BB47]      

DifficultyIsHard:
$1D/BB45 F6 12       INC $12,x  [$00:0912]   

General:
$1D/BB47 DA          PHX                     
$1D/BB48 A2 C3 F4    LDX #$F4C3              
$1D/BB4B 22 D4 98 00 JSL $0098D4[$00:98D4]   

And changing the above BEQ to a BRA like the first change almost fixes it, except the photos during the ending are invisible!

There's one final check for difficulty that determines visibility of the photos (?)

$1D/BBA8 A5 84       LDA $84    [$00:0084]   A:BB90 X:0900 Y:01BF P:envmxdizc
$1D/BBAA C9 04 00    CMP #$0004              A:0002 X:0900 Y:01BF P:envmxdizc
$1D/BBAD D0 2A       BNE $2A    [$BBD9]      A:0002 X:0900 Y:01BF P:eNvmxdizc <-- change this to NOP?

DifficultyIsHard:
$1D/BBAF A5 BC       LDA $BC    [$00:00BC]   A:0002 X:0900 Y:01BF P:eNvmxdizc
$1D/BBB1 F0 26       BEQ $26    [$BBD9]      A:0001 X:0900 Y:01BF P:envmxdizc


DifficultyIsEasyOrNormal:
$1D/BBD9 6B          RTL                     A:0002 X:0900 Y:01BF P:eNvmxdizc	; No photo

After changing the above BNE-on-hard-difficulty to NOPs, letting it fall through, we are treated to the ending:

This kind of implies some Easter Egg like they were maybe going to have different photo images show up for different difficulty settings. Or maybe there already is something like that, I honestly haven't spent a ton of time with this game. If I did maybe I would play it on Hard.

The full set of codes, which do the skips described above are:

05FD9DB3
1DB0F280
1DBB3C80
1DBBADEA
1DBBAEEA

or, you can play the Japanese version.

Or, you can download and apply this patch
https://cml-a.com/content/data/ContraIII.ips

which contains these changes, plus the change from this post. You can apply the patch using a tool such as LunarIPS or your emulator may apply it automatically if it's in a folder with a ROM with the same name. The patch is applied to the USA version SNES ROM.

September 15th, 2026 at 4:45 am | Comments & Trackbacks (0) | Permalink

📅July 28th, 2026

This is about a quick investigation.

Recently I got this Raspberry Pi Pico 2W as a toy:

It does come with a single blinking LED, you know, so you can check that basic things are working. So that's good. Since it's nice to actually display things, I got a small, simple display- a WaveShare 1.69" LCD display and hooked it up as follows:

WaveShare LCD pinRaspberry Pi Pico 2W semantic pinRaspberry Pi Pico 2W global pin
VCC3v3Pin 39
GNDGNDPin 38
DINGP19, SPI0 TXPin 25
CLKGP18, SPI0 SCKPin 24
CSGP17, SPI0 CSnPin 22
DCGP22Pin 29
RSTGP13 (also SPI1CSn)Pin 17
BLGP21Pin 27

The initial functions like powering it on, turning on the backlight and clearing the screen to white and black seemed to work. The only thing is, the colors were wrong. For example, if I tried to clear to RED, well, the sample code has

#define RED           0xF800

Except when I try it, it looks like blue.

Not a big deal, figure the red/blue channels are swapped. Very common problem. The documentation, regarding color formats, says

This LCD supports input color formats of 12-bit, 16-bit, and 18-bit per pixel, which are
RGB444, RGB565, and RGB666. This demo example uses the RGB565 color format,
which is also a commonly used RGB format.

That makes it sound like channel swapping is unlikely, and the demo is supposed to be RGB565, but who knows. Or it might be RGB444 with unused bits. I haven't worked with this device before and I couldn't find any clarifying documentation on the mode selection so I don't know.

So as another test, I try

#define GREEN         0x07E0

since that should work, irrespective of red-blue channel swapping. Right?

Wrong. Instead of being green, it looks red now. I guess it's good to know that the different color channels are capable of displaying, But what kind of absolute madman would put red in the middle of the format? It doesn't match any of the formats it listed support for, and seems like a very strange decision. Clearly something weird is going on.

As a mini investigation, I did a test where I turned each of the 16 bits of the image data on in turn, to see the effects.

At this point, I don't 100% know how many bits are in each channel or how the channels are ordered.

But, I was looking for

  • The highest-order red
  • The highest-order green
  • The highest-order blue

Since those probably have to exist somewhere, and if that 1 bit is turned on, you'll be able to see the effects. E.g., a dark red, a dark green, a dark blue.

Sure enough, I found them and marked them down:

// [0] - 
// [1] - 
// [2] - 
// [3] - blue (high)
// [4] - 
// [5] - 
// [6] - 
// [7] - 
// [8] - red (high)
// [9] - 
// [10]- 
// [11]- 
// [12]- 
// [13]- green (high)
// [14]- 
// [15]- 

The high-order blue was at bit 3, high-order red was at bit 8, and high-order green was at bit 13. This comports with the experiment earlier where setting the middle of the data would make it red. It all seems a bit weird but at least it's true and I checked it's reproducible.

If you notice, the blue is 5 bits away from the red, the red is 5 bits away from the green, and green appears to wrap around.

If you fill it all in, you get

// [0] - green 
// [1] - green 
// [2] - green (low)
// [3] - blue (high)
// [4] - blue
// [5] - blue
// [6] - blue 
// [7] - blue(low)
// [8] - red (high)
// [9] - red
// [10]- red
// [11]- red
// [12]- red(low)
// [13]- green (high)
// [14]- green 
// [15]- green 

Which is R5G6B5. Just... Shifted by 8 bits. Or with the bytes swapped around, if you prefer to look at it that way.

Sure enough, if I swap the bytes of the colors whenever I import them, you get the right result:

Looks like an endianness type of problem, since some codepaths for the demo do swap the bytes:

 void LCD_WriteData_Word(UWORD da)
{
    UBYTE i=(da>>8)&0xff;
    DEV_Digital_Write(DEV_CS_PIN,0);
    DEV_Digital_Write(DEV_DC_PIN,1);
    DEV_SPI_WRITE(i);
    DEV_SPI_WRITE(da);
    DEV_Digital_Write(DEV_CS_PIN,1);
}   

Just, not the one I was using. Oops!

I think it would make the most sense to store all your colors pre-swapped, so that you can simply copy them. Oh well.

Fixed this easily by swapping the bytes.

If you want to try out this demo, the code is available here: https://github.com/clandrew/rpball

July 28th, 2026 at 2:35 am | Comments & Trackbacks (0) | Permalink

📅April 7th, 2026

The MUD, also known as Aaezure Odyssey, is still online. It's accessible at

dwmud.org 1111

by telnet. Or also through its old address, dw.kicks-ass.org.

I posted about this game before:

With a heavy heart I tell you its administrator, Myst, passed away last year. One of the others in the DW community let me know. Her personality and work were so influential in my young years. She was really accomplished and so beloved by the community. I miss her a lot. I know so others in the community. There is some more information on the MUD's Facebook page:

https://www.facebook.com/DeathwishMUD

The MUD is still online now, maintained by others in the community (e.g., Fantar). The web site for the MUD itself is not online at the moment, so in addition to sharing my feelings and sentiments I wanted to share the information for that.

April 7th, 2026 at 12:32 am | Comments & Trackbacks (0) | Permalink

📅January 18th, 2026

You know this 2D water effect with a two-buffer trick:

https://www.gamedev.net/articles/programming/graphics/the-water-effect-explained-r915

I made a version of it that runs in ShaderToy (a web application that runs GLSL fragment shaders):

ShaderToy is very fun, just know it's not a fully programmable 3D pipeline.

If you browse people's samples for it you'll see people have done truly amazing things either in procedural geometry, or creating the appearance of 3D geometry using 2D approximations. Doing everything in screen space, lots of procedural rasterization of 2D shapes with no mesh. So anyway, if you're trying to port an algorithm that needs to sample from buffer A to write B, then swap the roles and read B write A, you have some open decisions on how to do it, since you don't have the usual level of flexibility over resource binding.

This method uses a hack of alternating writing to red and blue color channels of an intermediate. Since you can read from the target you're writing to, this way lets you read from the previous frame and the previous one to that.

You can see how it's working if you view the heightmap single channel only:

Or, viewing the raw heightmap data, you can see the two color channels together (with green sort of reserved as a debug channel):

The heightmap gets sampled and used to displace where an input texture of some rocks gets sampled. The end result is a 2D effect all in screenspace but it's a bit 3D looking.

You can see the demo here (works in most browsers):

https://www.shadertoy.com/view/t33yDf

January 18th, 2026 at 2:56 am | Comments & Trackbacks (0) | Permalink

📅January 9th, 2026

Suppose you know what your life is missing and that's Windows 98. You like the tactile feel of an actual machine. You don't want to use an emulator or a VM.

The good news is: yes, you can still set this up today. It's good to know what works and what doesn't, or what takes some effort.

This post describes a couple aspects of setting it up- drivers and networking- and what you can expect.

This is all general computer literacy tasks, not programming. Some of it isn't super obvious so I'm writing this for anyone that needs it.

Drivers

People don't like hearing this, but you'll have a lot better time using compatible hardware that could have reasonably shipped with Win 9x.

As in, hardware released at the time. Yes, you do see a lot of videos of "I set up Win95 on this new laptop", "I set up Win9x on this new gaming PC in a separate partition and got it enumerated in the bootloader", "Look, it works on this pocket organizer!" and it looks very impressive. There's one detail you'll usually see in those videos: they ONLY get so far as booting the OS. Maybe launch Paint and Calculator. They don't use network features, they don't use graphics acceleration, they might not even try to play sound, half the buttons don't work. They do horrible hacks, make delicate changes to system.ini, autoexec.bat, config.sys, try to fool the OS into enumerating different hardware caps. And they succeed, by some measurement of 'success', which is just booting the OS.

Machine seen at Vintage Computing Festival which I'm pretty sure did not OEM with Win95, at least not in English.

If your goal is just booting the OS and none of that other stuff, great.

If you want to use, say, really use the computer- use network features and connect to the Internet, you'll hit a wall. Use a non-PS2 mouse, you'll hit a wall. Potentially use the CD drive, you'll hit a wall. If you want to use the machine in any real way, you're limited. The OS doesn't live in a vacuum and it ultimately needs a way to talk to the hardware, and the inbox default drivers from the 90s are not future proof in the way you might be hoping. You probably want graphics, sound, network features from your hardware and those require drivers. Newer or unsupported hardware will not have Win 9x drivers and it is MUCH harder (technically possible sometimes but significantly harder) to hack your way to success around this.

Instead, why not go 100% immersion and use contemporary hardware?

Sound Blaster I got as junk on Ebay.

Get a Sound Blaster, get PS2 keyboard and mouse, get an old network card, get an old Voodoo, whatever, doesn't matter, get a low spec machine. I know it's a pain and there are some costs to doing this but you'll have to do way fewer hacks, and life will be better.

Wi-fi

Not strictly necessary, but it is nice to get some form of Internet connectivity. Depending on the layout of your home, you might not want to run a gigantic ethernet cable. The appealing option is wi-fi, then. You will have trouble using a real wi-fi dongle since there's unlikely to be compatible drivers. Also, in the OS, you'll notice Win9x can be configured against dialup and LAN and that's pretty much it. Any level of getting the OS to understand wi-fi directly needs to be provided by a driver. It will be hard to find that.

Fortunately, there's an actual connectivity method that's pretty easy.

Get one of these really inexpensive adapters that plug into an ethernet port, so your computer thinks it's LAN. Something like this.

The connection to the computer is LAN, but the connectivity to the internet is over wi-fi. You set it up in a local gateway, set up the SSID and password there as needed, then tell the OS it's ethernet, done. Of course do this at your own risk, there are risks using any kind of internet adapter product plus some others mentioned below. This method is generally pretty easy and in fact, you can do it this way for pretty much any old device that only supports Internet over ethernet.

The Web

There's the actual question about what to do once you're online.

This question holds regardless of whether you connect over wi-fi or some other way.

Maybe you want to view web sites in a browser. This, I think you shouldn't try it. It's possible but not worth it. I'll explain why.

There are a couple obstacles keeping your old Win9x box from browsing the modern Internet. For example, if you try Google:

Or Wikipedia:

They don't load, and it's because of SSL.

Tilde.club does not use SSL:

So it works but just looks a little screwy.

The inbox browser with Windows 98 is Internet Explorer 6. Some level of SSL can be enabled with Internet Explorer 6 as an optional feature or you can get it through other browsers. Still, it's not enough. SSL/TLS is not a yes or no. There are different versions and different encrypted key sizes and IE6's optional feature gives you only the most basic one. Other browsers will get you only slightly further along and that's it.

The exception is web sites that don't use SSL at all- there are only a handful of "WWW1" style sites around anymore, like tilde.club, and sometimes visiting them pops up all kinds of warnings in people's modern web browsers so they get avoided.

Note: People always talk about web pages as a platform as if they have this timeless works-everywhere quality. As if compat is king, compat was the utmost priority, and nobody ever deprecates older hardware.

Except, it's not true. The complete deprecation of old hardware has happened already.

The deprecation was for a good cause, to be clear. Without SSL, you're vulnerable to man-in-the-middle attacks. Someone could, say, set up their laptop at a coffee shop and create a hotspot for people to log into, and when you log into it, they can eavesdrop on credit cards, passwords, personal data, even change the communication that is sent to you or from you to others. But with SSL, this becomes a lot harder to the point where it's generally not done, and 'hacks' use some other mechanism. Despite what Youtubers shilling VPNs for 'security' will tell you, SSL by itself is actually very real and effective. The main benefits of those kinds of VPNs are usually location spoofing not security.

The security you have through SSL is effective against many kinds of threats, coming at a cost of deprecating old hardware.

Second, the modern internet may be locked away from you because of the raw computation needed to load most pages.

The minspec for the modern internet has significantly risen over time. And if you followed my setup advice and used old, contemporary hardware, the good news is that you can get your computer working- yet, it may be too slow in terms of computation. It's a trade-off. I still claim the slower hardware is worth it, but that's the trade-off.

You can look at the raw disk size of the downloaded parts of modern web pages to get a sense, and imagine the computation needed to parse client-side scripts that size. In a fully advertising-driven Internet where web sites are playing with cat-and-mouse with ad blockers, web sites have nested divs upon nested divs with insane amounts of client-side processing. And no one is writing minimal, static pages anymore, even if those could deliver the same functions. Simple pages with a menu and a splash screen use HTML5 canvas, virtualized scrolling, and modal dialogs have changed from 'physical' (actual new pop-up windows, that create a new browser window) to 'virtual' (a fake window with an 'X' button overlayed on top of the page). Web site design is very tangled up with fashion, fashion changes, you don't want your web site to not look cool, and the fashion changed in favor of forcing us to buy new computers.

That's why if you connect your Win9x box to today's modern internet straightforwardly, you will have trouble viewing most web sites, even fairly minimal-looking ones if they are modern, and sometimes even if they aren't particularly modern.

With that, my recommendation is to

  • Take a deep breath
  • Make peace with the fact that while you can connect to the internet, web browsing is not really accessible to you. Use the internet for things which are not in a web browser.

When people say they want to functionally get web browsing on Win98, I think they're misrepresenting things. Because in actuality, they want to travel back in time to use and view the web sites from back then.

Like, say you want to re-live an old online multiplayer game. With some effort you can functionally run the game. But then you log on and there are no other players. Is that really what you wanted?

Unlike local computing, the internet is full of the content of other people. Your experience depends on that completely.

You could think of the internet beyond web browsing. Your Win98 box could use it more for file sharing without having to write disks or CDs or set up a serial cable. That's super useful. I think a giant struggle to access a partially functional modern internet through Windows 98 is not really worth it. That's my opinion on it though.

Tunneling

If you still need some form of web browsing and will stop at nothing, there are a few options that rely on, basically, tunneling through a 2nd computer.

If it'd satisfy your craving, you could use a service like protoweb to access some curated selections of old sites from IE6.

Or you can use a portal like http://theoldnet.com, which does tunneling of some pre-blessed sites like Internet Archive and Wikipedia for you.

If you want to access any arbitrary site, a way to get pretty functional web browsing on Win9x OS today is to tunnel SSL content through a local web server that you own, and that local web server understands SSL. Then it sends back a page with no SSL to your retro machine.

For example, with this: https://github.com/snacsnoc/windows98-ie-proxy

Of course you need to make sure the connection between the Win98 box and the web server is trustworthy and be aware of security implications as you come across them.

That fixes the SSL part. It may only partially fix the 'lack of compute power' part, but is likely to be good enough.

Fun aside: Fujinet! to enable internet (not really web browsing) on very old 8-bit computers, people have simply been solving it in hardware+software, not software alone, through the Fujinet project which is very cool! I saw a demo of this at Vintage Computing Festival. You buy a hardware peripheral and plug it in and that provides the extra compute power for internet connectivity, among a whole slew of other things. For HTTPS it could definitely be used to view some text-based content.

The methods of accessing the internet through Windows 98 discussed here don't involve offloading processing onto a local peripheral but there is offloading is to a remote proxy if you want.

Security

There's additional nuance thinking about security in these environments. I already mentioned some things you need to consider when setting up internet.

You should know, and probably know already, Windows 98 doesn't really do separation of user privilege, everything is basically run as administrator. And more of the driver model sits in kernel mode. And no security updates for anything- neither in the OS, nor drivers. So in some ways, opening up your retro machine to the internet is dangerous and risky.

On the other hand, today's bad actors are not targeting Windows 98 systems. Today's browser malware likely won't be compatible with the web browsing solutions you're using. There are some common app vectors that get targeted- Discord, Teams, WhatsApp- those won't be running in this environment. So in some other respects it is not so risky.

And you probably aren't daily driving in it. Or maybe you are. Something something, joke about doing banking over telnet.

January 9th, 2026 at 11:46 am | Comments & Trackbacks (1) | Permalink

📅November 9th, 2025

This post is about a handy palette rotation art tool I made recently.

It's really simple, you create a static image (e.g., a PNG file) in an external editor of your choice and it colorizes the result into a multi-palette GIF image file. You can preview and scrub through the frames as you make adjustments. Comes in handy since there are not a lot of modern art tools (that is- Win32 and not DOS, say) that prioritize support for indexed color much less make color cycling easy.

Talking to retro pixel artists nowadays I found none of them do color cycling art. Even when I offered to commission them. They indicated to me lack of toolchain is part of the reason why. They told me some other reasons, like they just find it tedious or they're not used to working that way, fair enough. I think it's a dying art sadly. This tool fixes the toolchain part.

I avoided having to invent a new file format by having a kind of convention for representing palette data at the top of your source image. There's a mini specification for it. Basically, you list out a 'reference palette' that your framebuffer will be comprised of, and then a set of N palettes that will be used to color the final image. The result animation will have N frames. Each palette is prefixed by a magenta pixel, as a delimiter. This is convenient for easy editing and offers some flexibility in how you want it to appear in the document.

Example source image:

What it looks like in the program:

Result if you hit 'save':

I find it easier to use grayscale for cycled colors but obviously it's up to you.

You can do color cycling like for the water example above, or just a recoloring like the yellow lights.

Before doing this project, I didn't actually know that GIF supported multiple palettes since I never really looked into it. For most GIFs I found around the internet whenever I bothered to look, they used one global palette. And the tools I used for creating GIFs didn't expose options for multi-palette. But, it is supported in the underlying format. If you're using Windows Imaging Component to encode GIFs, use an indexed color format like GUID_WICPixelFormat8bppIndexed and call IWICBitmapEncoder::SetPalette on the frames to set it. That's how this tool does it anyway.

Link to project: https://github.com/clandrew/MultiPaletteGif/

You can download the latest release here.

November 9th, 2025 at 11:23 pm | Comments & Trackbacks (0) | Permalink

📅October 14th, 2025

For old closed source games, machine decompilation of the binary into C is not some magic pathway into full reverse-engineering. I am so sick of this hyper fixation on decompilation into C at all costs even if done by machine. It is fine but no it's not some magic pathway to having all your questions about a game answered.

It's like say you're being asked to write an essay doing character analysis of a Shakespeare play. And you think that translating the play into current-day English would fix all your problems.

To write the essay, you STILL face these open problems of "what this character's motivations are", "how do they relate to others", "why were they written this way". You still have these problems, whether the play's in Elizabethan English or not. The newer English might help, but that's it.

In the same way for a decompiled game, you still face these open problems of "what function is responsible for this behavior", "the data at this memory address, what does it mean", "what is the compression scheme for this data?". The decompilation might make it easier to read, but that's it.

For the essay, you might in fact prefer to do it in the original Elizabethan English instead of use the modern translation. Because, you can make connections based on wordplay and repeated motifs, and ensure nothing was lost in translation.

In the same way you might prefer using the original binary and not solely use decompilation. Because generally, you can't debug the decompilation. Most people don't try to re-compile the decompilation, and anyway the decompilation is not "round-trippable"- it doesn't produce something equivalent to the original when compiled, so you'd have no idea if you're debugging something representative. If it even works. (SOME PEOPLE DON'T COMPILE THE DECOMPILATION. I am losing my mind at this) So, everyone basically just does static analysis. And there's only so far you can get with static analysis.

For people who have machine decompilations of games, you can try asking them questions about the code. Although the code is supposedly in this easier form, they won't know. Or at least, they'd have to do some digging, which they'd have to do anyway even if they didn't have decompilation. Machine-decompiled code has placeholder everything- function names, data identifiers, labels. The code flow alone tells you nothing.

I guess I'm claiming decompilation can save you some work but it's not THAT much work, and no it's not some magic solution to all your problems.

The hyper fixation on "having complete source code in C" even if machine-generated is incredibly dumb and kind of irrational. There's a littering of projects that present themselves like they're "done" simply because they've done this. Or for any project, it is not regarded as "done" unless there's a full decompilation of the whole game into C, even if machine generated. Like say I use a machine translation to put a Shakespeare play into modern English, and then the result is littered with wrong grammar, misplaced slangs and nonsensical garbage and will need a ton of human massaging to get it anywhere close to legible. But hey it's in modern English!!!

I saw someone use tools to "decompile"-- I think it was [name redacted] for SFC, this project to reverse-engineer the game, and it was literally just a bunch of data rips and a machine generated decompilation. Like. Why start there?? The machine generated decompilation is not anything, how about you take a small piece, replace some symbols and decompile it yourself as a human.

As an aside, some people do decompilation as a desire to "uncover the mystery of the orignal source code", to feel like they've recovered the original source code, including for old games.

Well for SNES era games and earlier, the games were written in assembly language mostly since there wasn't yet a good C toolchain for those platforms. So if that's your motivation, you're better off doing dissassembly not decompilation. For 65816 the register width and addressing modes are a runtime decision between 8 and 16 bit, not a statically known decision, so you can not even robustly statically dissassemble but you can have tools that guess with a heuristic and possibly get it wrong sometimes if you are ok with that.

Also, I see some people use decompilation to recover data, which is kind of absurd. It's not necessary. You can just find the data and rip it, if uncompressed.

October 14th, 2025 at 4:04 am | Comments & Trackbacks (0) | Permalink