For console output, just log characters written to KPUTCHAR: 1000F180h
On the IOP, you can, at least initially, ignore load delay slots (but of course not branch delay slots). Also the EE doesn't have load delay slots.
You can start with either EE or IOP but you'll need both fairly soon. EE is more interesting but you have to do the IOP too. If you have a PSX emulator, that's a big boost, just grab its core, and add the things it's missing.
The BIOS, during bootup, will write to or read from all kinds of wacky addresses outside the documented registers. These can generally be ignored.
On the IOP, do HLE early and catch ioman:write checking for fd=1, you get some nice logging. There's plenty of information on IOP HLE in the discord, and if you're clever it can be done directly from the description in ps2tek (when you find jr + an addiu with a destination and source of $zr, scan backwards to find the import table).
The first padduw instruction has $zr for both inputs. Feel free to special case for a while (just don't forget to assert if the parameters aren't $zr).
The OS sets up a TLB mirror, mirroring 0xFFFF8000-0xFFFFFFFF down to 0x78000. This lets it access a few variables without a pointer, by doing accesses offset from zero register downwards. On Windows if you're using MapViewOfFile to implement mirrors, you should do a 64K mirror from FFFF0000 onwards to 0x70000, as 0x8000-0xFFFF is 32KB and the Windows mapping granularity is 64KB (this is ok because nothing normally accesses 0xFFFF0000 to 0xFFFF8000).
Once you reach a certain magic address (0x82000), load elf files, the obvious way, and set pc to point to the entry point. fire.elf and 3stars.elf are good to start with.
Take care to implement the SYSCALL instruction correctly on the EE, especially setting the CAUSE register. Otherwise the EE will end up in the weeds, leading to a very confused state. In your debugger, implement disassembly of the syscall instruction by looking at the preceding instruction to figure out which syscall exactly is being called.
The SIF CTRL register is not correctly documented in ps2tek. Writing 0x40000 on the EE should trigger an interrupt on the IOP side (seen in multiple emulators, assuming it's not just an emulator timing hack…). However, you can probably ignore this, it's not used by games generally.
Use a scheduler.
- A completely zero DMAtag is legal, it's a "refe" tag and can thus be the end of a chain. Similarly, a zero GIFtag is okay and is basically a nop.
- Vsync: It's checking the VSINT bit of CSR, and then writing to it which clears it. Then, it'll be set by your vsync-enter task.
- It uses overlapping blits to implement scrolling. Detect these and copy the memory to a second buffer and blit from that, if you want it to work. Unlikely to be needed by many games.
- Writes to DMAC registers with 64-bit instructions, not documented permitted in ps2tek. (sd/ld). Just forwarding to your 32-bit handlers is fine. Screen resolution is strange