934 rendered frames from one uninterrupted run: walk, run, turn, fire, open a door, walk through.
With pixels and time working, the next problem was input. There is no keyboard in simulation, so the testbench injects keys from a schedule file of cycle and keycode pairs, pressing and releasing through a 9-bit path where bit 8 marks a release. The guest acknowledges each key by writing to the key register at 0x014.
The first version of the bridge decoded every MMIO write, except that it only looked at port 0. The core dual-issues stores, so an acknowledge landing in slot 1 was silently dropped. The key_valid bit never cleared, and the guest re-read the same key on every poll.
The symptom was strange enough to be worth describing. Movement half worked, by luck of pairing, and firing never registered at all. A DOOM player who can walk but not shoot reads like a game bug, and it took a while to look one layer down.
The fix is to decode both ports in the MMIO task, servicing the older instruction first so paired UART stores also keep their order:
// mem_top_dg.v, after the fix: an MMIO store may arrive on either
// issue port, because the core dual-issues stores.
always @(posedge clk) begin
if (dg_enable && mem_we0 && mmio0)
mmio_write(mem_addr0[11:0], mem_wdata0);
if (dg_enable && mem_we1 && mmio1)
mmio_write(mem_addr1[11:0], mem_wdata1);
end
On a board the same rule applies, and it is the kind of thing that has to be right before anything else can be tested: a peripheral bridge must accept stores from both issue ports or the second one vanishes. Every action after this point depends on that fix.
Debugging routes came next. The player's own state is the thing that explains why a route fails, so the port prints a per-tic trace over the UART: leveltime, ready weapon, ammo, x, y, angle and the use button.
printf("[tk%05d] rw=%d ammo=%d x=%d y=%d ang=%d u=%d\n",
leveltime, (int)pl->readyweapon, (int)pl->ammo[am_clip],
pl->mo ? (int)(pl->mo->x >> FRACBITS) : 0,
pl->mo ? (int)(pl->mo->y >> FRACBITS) : 0,
pl->mo ? (int)(pl->mo->angle >> 24) : 0,
pl->usedown);
In simulation the UART is a 1 MiB buffer in the bridge flushed at exit; on a board it is ordinary printf to a serial console. This trace is what turned the door work from guesswork into measurement.
Input arrives as one action at a time to keep the captures readable: raise the pistol, walk forward, run forward, walk backward, turn left, turn right, strafe both ways, fire, switch weapons, punch, open the door. Thirteen scripted actions, each its own short run and its own clip.
The longer take is a single uninterrupted run of 934 rendered frames, about 26 seconds of game time at 35 tics per second. It walks out of the spawn room, runs the eastern corridor, turns into the dirt area, drops the trooper blocking the lane, pushes into the door recess, opens the door and walks through into the imp on the other side. Weapon raise, running, gunfire and the door sequence all flow out of one boot and one input file.
The schedule is a plain text file you can read and diff, and the simulation is deterministic given the ELF, the WAD and the schedule, so re-running the capture reproduces the clip. That determinism is what made the hardware bring-up comparable later: a frame produced on the board can be diffed against the frame the prototype produced from the same input.
Cost, stated honestly: one tic is about 1.62 million cycles, so one second of game time is about 57 million cycles, and the Verilator build sustained roughly 1.7 million cycles per second of wall time during these runs. One second of gameplay therefore costs about thirty seconds of wall clock, and the 26-second take is about twelve minutes of simulation. Live play against the prototype is slow motion, so the practical deliverable at this stage was offline recordings driven by precomputed schedules. The gap is a property of the verifier, not the design: a prototype is slower than the thing it prototypes.
M_Jagadeesh97
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.