https://mjg59.dreamwidth.org/73001.html
Account name: [ ] Password [ ] [Log in]
(OpenID?) (Forgot it?) [ ] Remember Me
You're viewing [personal profile] mjg59's journal
Create a Dreamwidth Account Learn More
[ ] [Interest ] [Go]
Reload page in style: site light
Matthew Garrett
Cordoomceps - replacing an Amiga's brain with Doom
Cordoomceps - replacing an Amiga's brain with Doom
Aug. 3rd, 2025 08:10 pm
[personal profile] mjg59
There's a lovely device called a pistorm, an adapter board that glues
a Raspberry Pi GPIO bus to a Motorola 68000 bus. The intended use
case is that you plug it into a 68000 device and then run an emulator
that reads instructions from hardware (ROM or RAM) and emulates them.
You're still limited by the ~7MHz bus that the hardware is running
at, but you can run the instructions as fast as you want.
These days you're supposed to run a custom built OS on the Pi that
just does 68000 emulation, but initially it ran Linux on the Pi and a
userland 68000 emulator process. And, well, that got me thinking. The
emulator takes 68000 instructions, emulates them, and then talks to
the hardware to implement the effects of those instructions. What if
we, well, just don't? What if we just run all of our code in Linux on
an ARM core and then talk to the Amiga hardware?
We're going to ignore x86 here, because it's weird - but most
hardware that wants software to be able to communicate with it maps
itself into the same address space that RAM is in. You can write to a
byte of RAM, or you can write to a piece of hardware that's
effectively pretending to be RAM[1]. The Amiga wasn't unusual in this
respect in the 80s, and to talk to the graphics hardware you speak to
a special address range that gets sent to that hardware instead of to
RAM. The CPU knows nothing about this. It just indicates it wants to
write to an address, and then sends the data.
So, if we are the CPU, we can just indicate that we want to write to
an address, and provide the data. And those addresses can correspond
to the hardware. So, we can write to the RAM that belongs to the
Amiga, and we can write to the hardware that isn't RAM but pretends
to be. And that means we can run whatever we want on the Pi and then
access Amiga hardware.
And, obviously, the thing we want to run is Doom, because that's what
everyone runs in fucked up hardware situations.
Doom was Amiga kryptonite. Its entire graphical model was based on
memory directly representing the contents of your display, and being
able to modify that by just moving pixels around. This worked because
at the time VGA displays supported having a memory layout where each
pixel on your screen was represented by a byte in memory containing
an 8 bit value that corresponded to a lookup table containing the RGB
value for that pixel.
The Amiga was, well, not good at this. Back in the 80s, when the
Amiga hardware was developed, memory was expensive. Dedicating that
much RAM to the video hardware was unthinkable - the Amiga 1000
initially shipped with only 256K of RAM, and you could fill all of
that with a sufficiently colourful picture. So instead of having the
idea of each pixel being associated with a specific area of memory,
the Amiga used bitmaps. A bitmap is an area of memory that represents
the screen, but only represents one bit of the colour depth. If you
have a black and white display, you only need one bitmap. If you want
to display four colours, you need two. More colours, more bitmaps.
And each bitmap is stored in an independent area of RAM. You never
use more memory than you need to display the number of colours you
want to.
But that means that each bitplane contains packed information - every
byte of data in a bitplane contains the bit value for 8 different
pixels, because each bitplane contains one bit of information per
pixel. To update one pixel on screen, you need to read from every
bitmap, update one bit, and write it back, and that's a lot of
additional memory accesses. Doom, but on the Amiga, was slow not just
because the CPU was slow, but because there was a lot of manipulation
of data to turn it into the format the Amiga wanted and then push
that over a fairly slow memory bus to have it displayed.
The CDTV was an aesthetically pleasing piece of hardware that
absolutely sucked. It was an Amiga 500 in a hi-fi box with a
caddy-loading CD drive, and it ran software that was just awful.
There's no path to remediation here. No compelling apps were ever
released. It's a terrible device. I love it. I bought one in 1996
because a local computer store had one and I pointed out that the
company selling it had gone bankrupt some years earlier and literally
nobody in my farming town was ever going to have any interest in
buying a CD player that made a whirring noise when you turned it on
because it had a fan and eventually they just sold it to me for not
much money, and ever since then I wanted to have a CD player that ran
Linux and well spoiler 30 years later I'm nearly there. That CDTV is
going to be our test subject. We're going to try to get Doom running
on it without executing any 68000 instructions.
We're facing two main problems here. The first is that all Amigas
have a firmware ROM called Kickstart that runs at powerup. No matter
how little you care about using any OS functionality, you can't start
running your code until Kickstart has run. This means even
documentation describing bare metal Amiga programming assumes that
the hardware is already in the state that Kickstart left it in. This
will become important later. The second is that we're going to need
to actually write the code to use the Amiga hardware.
First, let's talk about Amiga graphics. We've already covered
bitmaps, but for anyone used to modern hardware that's not the
weirdest thing about what we're dealing with here. The CDTV's chipset
supports a maximum of 64 colours in a mode called "Extra Half-Brite",
or EHB, where you have 32 colours arbitrarily chosen from a palette
and then 32 more colours that are identical but with half the
intensity. For 64 colours we need 6 bitplanes, each of which can be
located arbitrarily in the region of RAM accessible to the chipset
("chip RAM", distinguished from "fast ram" that's only accessible to
the CPU). We tell the chipset where our bitplanes are and it displays
them. Or, well, it does for a frame - after that the registers that
pointed at our bitplanes no longer do, because when the hardware was
DMAing through the bitplanes to display them it was incrementing
those registers to point at the next address to DMA from. Which means
that every frame we need to set those registers back.
Making sure you have code that's called every frame just to make your
graphics work sounds intensely irritating, so Commodore gave us a way
to avoid doing that. The chipset includes a coprocessor called
"copper". Copper doesn't have a large set of features - in fact, it
only has three. The first is that it can program chipset registers.
The second is that it can wait for a specific point in screen
scanout. The third (which we don't care about here) is that it can
optionally skip an instruction if a certain point in screen scanout
has already been reached. We can write a program (a "copper list")
for the copper that tells it to program the chipset registers with
the locations of our bitplanes and then wait until the end of the
frame, at which point it will repeat the process. Now our bitplane
pointers are always valid at the start of a frame.
Ok! We know how to display stuff. Now we just need to deal with not
having 256 colours, and the whole "Doom expects pixels" thing. For
the first of these, I stole code from ADoom, the only Amiga doom port
I could easily find source for. This looks at the 256 colour palette
loaded by Doom and calculates the closest approximation it can within
the constraints of EHB. ADoom also includes a bunch of CPU-specific
assembly optimisation for converting the "chunky" Doom graphic buffer
into the "planar" Amiga bitplanes, none of which I used because (a)
it's all for 68000 series CPUs and we're running on ARM, and (b) I
have a quad core CPU running at 1.4GHz and I'm going to be pushing
all the graphics over a 7.14MHz bus, the graphics mode conversion is
not going to be the bottleneck here. Instead I just wrote a series of
nested for loops that iterate through each pixel and update each
bitplane and called it a day. The set of bitplanes I'm operating on
here is allocated on the Linux side so I can read and write to them
without being restricted by the speed of the Amiga bus (remember,
each byte in each bitplane is going to be updated 8 times per frame,
because it holds bits associated with 8 pixels), and then copied over
to the Amiga's RAM once the frame is complete.
And, kind of astonishingly, this works! Once I'd figured out where I
was going wrong with RGB ordering and which order the bitplanes go
in, I had a recognisable copy of Doom running. Unfortunately there
were weird graphical glitches - sometimes blocks would be entirely
the wrong colour. It took me a while to figure out what was going on
and then I felt stupid. Recording the screen and watching in slow
motion revealed that the glitches often showed parts of two frames
displaying at once. The Amiga hardware is taking responsibility for
scanning out the frames, and the code on the Linux side isn't
synchronised with it at all. That means I could update the bitplanes
while the Amiga was scanning them out, resulting in a mashup of
planes from two different Doom frames being used as one Amiga frame.
One approach to avoid this would be to tie the Doom event loop to the
Amiga, blocking my writes until the end of scanout. The other is to
use double-buffering - have two sets of bitplanes, one being
displayed and the other being written to. This consumes more RAM but
since I'm not using the Amiga RAM for anything else that's not a
problem. With this approach I have two copper lists, one for each set
of bitplanes, and switch between them on each frame. This improved
things a lot but not entirely, and there's still glitches when the
palette is being updated (because there's only one set of colour
registers), something Doom does rather a lot, so I'm going to need to
implement proper synchronisation.
Except. This was only working if I ran a 68K emulator first in order
to run Kickstart. If I tried accessing the hardware without doing
that, things were in a weird state. I could update the colour
registers, but accessing RAM didn't work - I could read stuff out,
but anything I wrote vanished. Some more digging cleared that up.
When you turn on a CPU it needs to start executing code from
somewhere. On modern x86 systems it starts from a hardcoded address
of 0xFFFFFFF0, which was traditionally a long way any RAM. The 68000
family instead reads its start address from address 0x00000004, which
overlaps with where the Amiga chip RAM is. We can't write anything to
RAM until we're executing code, and we can't execute code until we
tell the CPU where the code is, which seems like a problem. This is
solved on the Amiga by powering up in a state where the Kickstart ROM
is "overlayed" onto address 0. The CPU reads the start address from
the ROM, which causes it to jump into the ROM and start executing
code there. Early on, the code tells the hardware to stop overlaying
the ROM onto the low addresses, and now the RAM is available. This is
poorly documented because it's not something you need to care if you
execute Kickstart which every actual Amiga does and I'm only in this
position because I've made poor life choices, but ok that explained
things. To turn off the overlay you write to a register in one of the
Complex Interface Adaptor (CIA) chips, and things start working like
you'd expect.
Except, they don't. Writing to that register did nothing for me. I
assumed that there was some other register I needed to write to
first, and went to the extent of tracing every register access that
occurred when running the emulator and replaying those in my code.
Nope, still broken. What I finally discovered is that you need to
pulse the reset line on the board before some of the hardware starts
working - powering it up doesn't put you in a well defined state, but
resetting it does.
So, I now have a slightly graphically glitchy copy of Doom running
without any sound, displaying on an Amiga whose brain has been
replaced with a parasitic Linux. Further updates will likely make
things even worse. Code is, of course, available.
[1] This is why we had trouble with late era 32 bit systems and 4GB
of RAM - a bunch of your hardware wanted to be in the same address
space and so you couldn't put RAM there so you ended up with less
than 4GB of RAM
Tags:
* advogato,
* fedora
* Previous Entry
* Add Memory
* Share This Entry
* Next Entry
---------------------------------------------------------------------
* 5 comments
* Reply
---------------------------------------------------------------------
Flat | Top-Level Comments Only
no subject
Date: 2025-08-05 09:15 am (UTC)
lnr: Halloween 2023 (Default)
From: [personal profile] lnr
I love it.
* Link
* Reply
no subject
Date: 2025-08-05 02:16 pm (UTC)
bens_dad: (Default)
From: [personal profile] bens_dad
Easter 1983 IIRC, I saw an HP mini computer with a hardware emulation
of a 6502, driving a BBC micro.
Not sure whether it was software development overkill, or a debugger
of hardware add-ons.
Now the successor of the BBC is powerful enough to emulate a CPU
which succeeded the one in the original BBC micro.
* Link
* Reply
no subject
Date: 2025-08-05 06:16 pm (UTC)
rhialto: Me under a waterfall (Default)
From: [personal profile] rhialto
Once upon a time I wrote a little program for my A1000 to keep the
KickStart RAM writable and the boot ROM mapped in memory. I see I
used a clever trick to do a privilege escalation by using the
privilege violation vector. Sneaky!
I seem to recall that making the KickStart RAM read/only is done (in
the boot ROM) by writing somewhere to the boot ROM address range. And
that the KickStart is set up in such a way that it can also function
as the boot rom, probably even at the boot rom address. I suppose you
have Marcus Wandel's kickstart disassembly which illustrates that.
;;;;
;
; KSRAM - Un-writeprotect KickStart RAM, then reboot.
; Leaves KS RAM unprotected and BOOT ROM at $F80000.
;
; (C) Copyright 1988 by Olaf Seibert, KosmoSoft.
; May not be used for any commercial purpose.
;
; Does nothing when the KickStart RAM already is unprotected,
; so you can put it in your startup-sequence.
;
; Only works with an Amiga 1000, obviously.
; Works with Kickstart 1.1, 1.2 and 1.3, and supposedly with
; newer versions as well.
;
far code
far data
privv equ $20 ; privilige violation vector
kickstart equ $fc0000
kickentry equ kickstart+4 ; kickstart entry vector
;;;;
;
; Test if the KS RAM-ROM already is RAM.
;
test1
move.w kickstart,d2 ; Keep original contents
move.w #$5555,d3 ; first test pattern
move.w d3,kickstart
cmp.w kickstart,d2 ; RAM/ROM unchanged?
beq.s test2 ; do one more test
cmp.w kickstart,d3 ; Is it what we put there?
bne.s badram ; must be bad RAM.
test2
asl.w #1,d3 ; second test pattern
move.w d3,kickstart
cmp.w kickstart,d2 ; RAM/ROM still unchanged?
beq.s rom ; must be ROM.
cmp.w kickstart,d3 ; Is it what we put there?
beq.s goodram ; equal? Must be RAM
;;;;
;
; The RAM test indicates the RAM is bad. This should never
; happen, but we exit anyway to prevent a rebooting loop.
;
badram
move.w d2,kickstart ; restore contents
moveq.l #20,d0 ; BAD RAM! FAIL MISERABLY!
rts
;;;;
;
; The RAM test indicates we have already good RAM.
; There is nothing we need to do, so we just exit now.
;
goodram
move.w d2,kickstart ; restore contents
clr.l d0 ; succeed
rts
;;;;
;
; Now is the time for a message. We'll be right back!
;
credits
dc.b 'KSRam 1.0 (20 december 1988)',$0d,$0a
dc.b 'Copyright (C) 1988 Olaf Seibert, Kosmosoft',$0d,$0a,0
;;;;
;
; And don't forget...
;
dc.b 'Commodore AMIGA ROM Bootstrap',$0d,$0a
dc.b 'Copyright (C) 1985, Commodore Amiga, Inc.',$0d,$0a
dc.b 'All rights reserved',$0d,$0a,0
ds.w 0
;;;;
;
; There is no RAM, as far as we can see; it refuses to
; change, whatever we write to it. It must be ROM.
; So even if we are running in an A500 or A2000, we'll reboot.
; Too bad... no way to prevent a reboot loop if you are dumb
; enough to put this in your startup-sequence.
;
rom
move.l kickentry,a2 ; Get kickstart entry point
lea -2(a2),a2 ; There is a RESET instruction
; just before the entry point
move.l a2,privv ; install handler
move.w #$2700,sr ; force privilige violation.
jmp (a2) ; What? No exception yet?
; We'll do it by hand, then!
;;;;
;
; That's all, folks!
;
* Link
* Reply
no subject
Date: 2025-08-05 10:39 pm (UTC)
jwz: (Default)
From: [personal profile] jwz
This is great stuff. If you haven't seen it, you might also enjoy
PiTrex https://www.ombertech.com/pitrex.php which is similarly mad:
it runs MAME on a Pi Zero using a custom cartridge to get access to
the Vectrex screen and controller.
* Link
* Reply
no subject
Date: 2025-08-06 04:08 pm (UTC)
emperor: (Default)
From: [personal profile] emperor
Excellent (if cursed) work :)
* Link
* Reply
* Previous Entry
* Add Memory
* Share This Entry
* Next Entry
* 5 comments
* Reply
Flat | Top-Level Comments Only
Profile
Matthew Garrett
About Matthew
Power management, mobile and firmware developer on Linux. Security
developer at nvidia. Ex-biologist. Content here should not be
interpreted as the opinion of my employer. Also on Mastodon and
Bluesky.
Page Summary
* [personal profile] lnr - (no subject)
* [personal profile] bens_dad - (no subject)
* [personal profile] rhialto - (no subject)
* [personal profile] jwz - (no subject)
* [personal profile] emperor - (no subject)
Expand Cut Tags
No cut tags
Top of page