RTG: Emulate the Z3660 video board#212
Conversation
LinuxJedi
left a comment
There was a problem hiding this comment.
Note
This is an AI code review performed by Claude Code (Fable 5), posted on behalf of @LinuxJedi.
Review: z3660: emulate the Z3660 RTG board (Picasso96)
Overview
Adds a new Zorro III device (src/z3660.rs, ~1360 lines) modelling the Z3660 accelerator's FPGA RTG core: autoconfig identity, the 32-bit register file at window offsets 0x100-0x7FF, 64 MB of backed board RAM, the GFXData mailbox blitter executor (fill/copy/invert/template/pattern/line/planar), palette capture, a 60 Hz vblank phase in emulated colour clocks, and a hardware-sprite mouse pointer composited at scanout. Presentation is hooked into both the window path (render_rtg_frame_if_active) and the CCP capture path (render_frame in control/exec.rs). Config plumbing ([z3660] enabled) follows the A2065 pattern exactly.
Overall: high-quality, well-structured work that fits the codebase's conventions. The tricky integration points were checked against the base tree and hold up. The blockers are stale documentation that contradicts the code, and two guest-controllable unbounded allocations.
Verified as correct
- Vblank timing under tick batching:
is_idle()defaults totrue, but Zorro devices are ticked unconditionally intick_timed_devices, and the deferred batch is flushed before every device-register read (flush_timed_devices), soVBLANK_STATUSadvances correctly andWaitVerticalSynccannot hang. Batching is exact sincetickis linear in cck. - Presentation buffer reshaping: the RTG path resizes
present_fbtoh * FB_WIDTH(possibly smaller or larger than the nativeFB_WIDTH * OUT_HEIGHT). This is safe:copy_present_frame/save_present_framescale arbitrarysrc_rowsviascaled_source_row, the sync native path resizes per frame (refresh_present_from_deinterlacer), and the threaded path replaces the whole Vec. - Threaded-render race handling: draining in-flight chipset results before the RTG compose is the right order -- a stale native frame cannot land on top of an RTG frame. At worst there is a one-frame native flash at the switch boundary, which self-corrects.
- CCP capture consistency: both
render_frameconsumers slicefb[..FB_WIDTH * lines], matching the RTG buffer's exact size. - Save states:
BoardDevice::Z3660is correctly appended last (bincode variant-index rule); no existing struct changes shape, so noSTATE_VERSIONbump is needed.MachineDescriptorcarries no board fields, consistent with existing boards. - Byte-order plumbing (the trickiest part): the guest-order pixel stores, the 0x00RRGGBB palette/sprite-colour representation, and the RGBA-low-byte-R presentation output are all mutually consistent, including the sprite composite swizzle. The two-halfword register-landing rule (
off + size == aligned + 4) correctly handles 68k split 32-bit writes. - 128 MB is a valid Zorro III size; the register-file window shadows the first 2 KB of RAM harmlessly.
Issues
1. Documentation contradicts the code (should fix before merge)
The user-facing docs describe an earlier iteration of the branch:
src/config.rs-- bothConfig::z3660andRawZ3660doc comments say "Bring-up stub: registers are logged, no display output yet." The PR ships a full display pipeline.copperline.example.toml-- says "planar blits, line draws, and surface ops are not implemented yet", butOP_P2C/OP_P2DandOP_DRAWLINEare implemented (the PR description confirms this). Only the ACC_OP surface ops are stubbed.docs/guide/configuration.md-- same stale list, plus the "RTG board stub" heading.src/emulator.rs-- the build comment andinfo!line also say "stub ... registers logged".
The module doc in z3660.rs is accurate; the rest should be reconciled to it. Given the project's docs-in-same-change rule, this is a merge blocker. Minor: consider a line in docs/zorro.md alongside the A2065 section.
2. Unbounded guest-controlled allocations (robustness)
Everything in the executor is carefully bounds-checked per byte except two allocations sized directly from mailbox fields:
OP_COPYRECT's overlap snapshot:vec![0u8; x1 * y1 * bpp]-- up to 65535 x 65535 x 4 = ~17 GB from one doorbell write. A buggy or hostile guest program aborts the emulator host.OP_SPRITE_BITMAP:vec![0u8; w * h]--w <= 16 * hires, buthires = y2 + 1can be 65536, so this does not actually boundw; up to ~4 GB.
rtg_frame already clamps to 4096x4096; the executor should clamp rect/sprite dimensions similarly (real VRAM bounds them on hardware). This also caps the synchronous-blit stall time (a 65535x65535 FILLRECT is ~4.3 billion iterations on the doorbell write).
3. Logging volume and cost (style/perf)
- Every register read/write logs at
info!(only VBLANK is demoted totrace!), andlog_gfxdatalogs every blit op atinfo!-- on a live Workbench desktop that is a firehose at the default level, for every user with the board enabled, not just driver developers. log_gfxdatabuilds threeVec<u16>s and does a dozen big-endian reads before the macro, so the cost is paid even when the level is filtered out -- on every blit.
Suggestion: keep info! for the one-time init probes (FW_VERSION, mode set, vidmode params), demote steady-state register traffic and per-blit mailbox dumps to debug!/trace!, and gate the log_gfxdata field extraction behind log::log_enabled!.
4. Minor points
- Frozen display edge case:
render_rtg_frame_if_activereturnsSome(false)whenrtg_active()is true butrtg_framecomposesNone(e.g.MODEwritten beforeORIG_RES, or an invalid colormode). That suppresses the chipset render too, freezing the last frame indefinitely rather than falling back to native output. ReturningNoneon compose failure would degrade more gracefully. - Main-thread compose cost: the RTG path bypasses the render worker; at 1920x1200 that is ~2.3M per-pixel colormode matches plus a horizontal nearest scale per frame, synchronously. Fine for now, but worth a note next to the "pixel-perfect presentation" follow-up. Hoisting the colormode dispatch out of the pixel loop in
rtg_framewould be a cheap win. - Save-state size: the 64 MB
vramrides the zlib stream. Mostly-zero VRAM compresses to almost nothing, but a busy 32-bit 1920x1200 desktop adds real megabytes and 64 MB of compressor work per save. Acceptable; worth knowing. RECT_PATTERNassumesuser0is a power of two (& (pat_rows - 1)); if the firmware guarantees this, a comment saying so would help.
Test coverage
Good breadth for a first pass: palette capture, activation gating, CLUT/565/32-bit scanout, pan, fillrect edges, overlapping copyrect, template JAM2, P2C and P2D decode, and the full sprite lifecycle. Gaps worth closing, since these regress silently:
OP_DRAWLINE-- especially the COMPLEMENT XOR path the shell cursor depends on, and pattern rotation.OP_RECT_PATTERN,OP_INVERTRECT,OP_COPYRECT_NOMASK(thepitch1/src-surface variant).- 15-bit (R5G5B5) scanout decode -- the only pixel format with no test.
- The split-write landing rule: two halfword writes high-first firing
reg_writtenexactly once. compose_rtg_presentscaling (bothw < FB_WIDTHandw > FB_WIDTH).
Security
The Zorro window is guest-facing attack surface; apart from the two allocations in issue 2, all VRAM accesses are bounds-checked and out-of-range accesses are dropped and logged. No host filesystem/network exposure. Determinism is preserved (all state advances on emulated cck), so headless runs and replay stay byte-identical.
Verdict
Approve once issue 1 (stale docs) and issue 2 (allocation clamps) are addressed; issue 3 and the test gaps are strongly recommended but could be fast follow-ups.
🤖 Generated with Claude Code
LinuxJedi
left a comment
There was a problem hiding this comment.
Note
This is an AI code review performed by OpenAI Codex, posted on behalf of @LinuxJedi.
I found eight correctness issues not already covered by Claude's review. They affect panning, inverse drawing, line rasterization, planar conversion, doubled sprites, save-state compatibility, and cold-reset behavior. Requesting changes until these are addressed.
There was a problem hiding this comment.
Pull request overview
Adds initial emulation and presentation support for the Z3660 RTG (Picasso96) board as a Zorro III autoconfig device, including scanout/blitter behavior and frontend integration so RTG modes can be displayed (and captured) alongside existing chipset output.
Changes:
- Introduces a new
Z3660Zorro device implementation (register file, backed VRAM, basic mailbox/blitter ops, scanout, sprite overlay) and wires it into the Zorro chain/device enum. - Adds RTG-aware presentation paths: native-resolution GPU texture overlay in the desktop window and RTG-aware headless capture/screenshot composition.
- Adds configuration/docs plumbing (
[z3660].enabled), plus save-state version bump for the newBoardDevicevariant.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/zorro.rs | Adds a BoardSpec::z3660() helper to describe the autoconfig board. |
| src/zorro_device.rs | Appends BoardDevice::Z3660 and wires dispatch into the device trait methods. |
| src/z3660.rs | New Z3660 RTG device implementation + unit tests. |
| src/video/window/rtg_texture.rs | New GPU texture/pipeline to present RTG frames at native resolution. |
| src/video/window.rs | Integrates RTG composition + GPU overlay into the window render path. |
| src/video/present_common.rs | Adds RTG-to-screenshot presentation composition helper. |
| src/savestate.rs | Bumps save-state version for the new BoardDevice variant. |
| src/lib.rs | Exposes the new z3660 module. |
| src/emulator.rs | Fits the Z3660 board onto the Zorro chain when enabled in config. |
| src/control/exec.rs | Makes headless capture prefer RTG output when RTG is active. |
| src/config.rs | Adds [z3660] enabled parsing and Config::z3660 flag. |
| src/bus.rs | Adds Bus::rtg_active() and Bus::rtg_frame() queries. |
| docs/guide/configuration.md | Documents the new [z3660] configuration block. |
| copperline.example.toml | Adds an example snippet for enabling [z3660]. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Autoconfigs the Z3660 FPGA RTG core (mfr 0x144B, product 1, 128 MB Zorro III window) so the open-source Z3660.card P96 driver finds it. Registers are latched and logged by name; FW_VERSION, INT_STATUS and MONITOR_SWITCH answer the FindCard/SetSwitch probes, and the VRAM and GFXData region is backed RAM. No display output yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
VBLANK_STATUS now follows a 60 Hz frame phase ticked in colour clocks instead of flipping on every read. Doorbell writes decode the GFXData mailbox (op name, offsets, rects, pitch, colormode, mask, minterm), and MODE / ORIG_RES / CUSTOM_VIDMODE writes log the modeline by parameter name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Capture the palette from the OP_DATA/OP upload stream (primary and secondary banks), the framebuffer start and width from the PAN op, and the display switch from MODE + OP_CAPTUREMODE (the Z3660 has no CIA pass-through switching; capture mode showing native video is the emulator presenting chipset output as usual). rtg_frame() composes the panned VRAM as presentation pixels in all four RGB formats. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The window presentation and the CCP capture path both check the board before rendering the chipset frame: when a mode is set and video capture is off, the panned VRAM framebuffer is composed into the presentation buffer (scaled into the FB_WIDTH stride) instead, falling back to native chipset output when capture mode is on. Drained render worker results propagate their outcome so redraw scheduling still sees chipset frames. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FILLRECT, COPYRECT (overlap-safe, 8-bit plane mask), COPYRECT_NOMASK (SRC minterm), INVERTRECT, RECT_TEMPLATE and RECT_PATTERN (JAM1/JAM2/COMPLEMENT + INVERSVID), synchronously on the doorbell as the bus-stalling hardware does. Colours are handled in guest byte order (the LE ARM reads the 68k values byteswapped), and the pitch field is longwords for fill/copy/invert but raw bytes for template/pattern, matching the firmware. DRAWLINE, P2C/P2D, ACC_OP, and the sprite remain logged stubs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SetSprite shows/hides via REG_SPRITE_BITMAP, OP_SPRITE_BITMAP decodes the 2-plane pointer image from the template scratch, OP_SPRITE_COLOR sets pens 1-3 (driver sends idx+1, bytes B,G,R), OP_SPRITE_XY tracks the position. The pointer is drawn over the composed frame at present time and never touches VRAM, like the real output-stage overlay. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DRAWLINE walks the signed deltas with the 16-bit rotating pattern (JAM1/JAM2, COMPLEMENT inverting the destination -- the shell XOR cursor). P2C decodes the staged bitplanes to pens; P2D looks pens up in the 256-entry destination-format CLUT staged ahead of the planes. Minterms are the firmware 0-15 enum (P96 sends 12 = SRC); only SRC/NOTSRC are executed, the rest log. This completes the ops Workbench uses: icons and window gadget imagery render on RTG. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mode set, modeline parameters, and resolution stay at info; the register access trace and GFXData op dumps move to debug now that the driver contract is mapped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The firmware encodes INVERSVID in bit 2 (rtg/gfx.h: JAM in bit 0, COMPLEMENT in bit 1, INVERSVID in bit 2). Using 0x08 meant inverse video was never triggered for valid draw modes, so inverse text and patterned drawing rendered without their source inversion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The config docs, the emulator build comment/log line, and the configuration guide still described the early "bring-up stub, no display" state. Describe the working RTG board instead, keeping the user-facing docs free of hardware internals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Appending BoardDevice::Z3660 changes the bincode enum shape, so a state holding a Z3660 board fails to decode on a build without the variant. Bump the version so the mismatch is reported cleanly instead of as a deep bincode error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sprite decoder rejected any pointer wider than 16 pixels, so a doubled (BIF_BIGSPRITE) or 32-wide pointer -- the OS 3.2 32x48 pointer uses one -- was cleared and the mouse pointer vanished while position tracking kept working. Decode the 2-bitplane image with the plane-0 then plane-1 row layout from the firmware, and scale a doubled sprite from its half-size source to 2x2 output blocks. Clamped to the firmware 32x48 buffer bound. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
render_rtg_frame_if_active returned Some(false) when the board was active but the frame failed to compose (e.g. MODE written before ORIG_RES), which suppressed the chipset render and froze the display on the last frame. Return None so the native path renders instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The GFXData rect width/height fields are guest-controlled, so a bogus op could size the COPYRECT overlap snapshot at up to ~17 GB or spin a multi-billion-iteration fill on the doorbell write. Clamp the extents to a sane maximum (shared with the framebuffer bound); no real screen or blit approaches it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BlitPlanar2Chunky does not set the mailbox colormode, so it can carry a stale 16/32-bit value from a prior op. P2C output is always one chunky byte per pixel, so use a fixed 1-byte destination stride rather than the colormode-derived bpp; otherwise the pixels are written on a 2- or 4-byte stride. The regression test now uses a stale 16-bit colormode on the P2C op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bus::power_on_reset zeroes the machine RAM, but the board device reset left VRAM (guest-readable board RAM) untouched, so a cold boot could see stale framebuffer data. Clear it in reset(); a warm reset re-inits the board and repaints, so clearing there too is harmless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The RTG window path composited the board frame into the fixed FB_WIDTH presentation buffer, so a mode wider than that buffer -- or shown in a window larger than it -- was sampled from a lower-resolution intermediate and looked soft, worst at high resolutions on a HiDPI display. Give the native frame its own GPU texture at the mode real resolution and draw it, linear-filtered, straight into the display sub-rect of the surface in the pixels render_with pass, after the UI buffer: a single hardware scale from native pixels to the physical display rect, at full fractional-scale resolution, with no shared-texture cap. The UI, cursor mapping, and the screenshot path (still reading present_fb) are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The P96 DrawLine specification requires Bresenham rasterization and makes Line.Length authoritative over the raw dX/dY -- that is how a clipped line segment draws the right pixel count. The board op interpolated between the endpoints instead and ignored Length (mailbox user[0]), so diagonals landed on the wrong pixels and a clipped segment overshot its extent. Step the line with Bresenham and honour Length, keeping the 16-bit pattern rotation and the JAM1/JAM2/COMPLEMENT/INVERSVID handling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pixel past the end of a row addressed the start of the next one, so a line overrunning the right edge wrapped around instead of being clipped. Found while validating DrawLine against P96's own software rasteriser with p96cts, a conformance suite for P96 card drivers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SetPanning passes signed x/y viewport offsets in x[0]/y[0] and the bitmap's row width in x[1]. Both were ignored: the offsets never reached the scanout base, and x[1] overrode the visible width, so a bitmap wider than the display mode scanned its whole virtual row into the window. Fold the offsets into the scanout base, keep the visible size from ORIG_RES, and use the width only as the row stride. The sprite needs no adjustment: SetSpritePosition already sends viewport coordinates. The firmware derives the base by shifting the offsets by the colormode, which matches the pixel size except for 15-bit, where it strides 8 bytes per pixel. We scan by the pixel size instead: P96's semantics are what a driver is checked against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nearest-neighbour copy indexed the left edge of each output pixel's source span, so `x * w / FB_WIDTH` topped out below `w - 1` whenever the board frame was wider than FB_WIDTH: the rightmost source columns never reached a downscaled screenshot. Sample the centre of the span instead, which reaches the last column and keeps the sampling unbiased. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two ways a driver could reach past the blitter's intent: The surface bases added VRAM_OFFSET to a guest 32-bit offset in usize. usize is 32 bits on wasm32, where an offset near the top of the address space wrapped to 0x1FFFF0 -- inside the mailbox region -- and corrupted it. Widen to u64 and saturate, so an out-of-range base stays out of range and every access below fails its bounds check. COPYRECT snapshotted the whole source rect to survive overlap. x1/y1 are clamped to MAX_DIM apiece, so that reached 64 MiB per blit for a pair of guest-supplied numbers. Buffer a single row instead and walk rows away from the destination, which keeps overlap correct at a bounded cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`[z3660] enabled` named the board in the section and repeated it in the key, and left no room for a second card without a second section. Follow [scsi] controller instead: one [rtg] section whose `card` names the board, "none" by default. A machine takes at most one RTG card -- screens come from whichever the Picasso96 driver finds -- so this stays a single key rather than a set of per-board sections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A cycle row alongside Chipset, so the board can be fitted without hand- editing a config file. Writes [rtg] card only when it differs from the profile default, as the other rows do, keeping saved files minimal. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The RTG texture is drawn after the CPU frame into the same display region, so a menu or panel was painted over and vanished: the controls still hit-tested, they just could not be seen. Present through the CPU path while the UI is up, which composites it on top as usual. That path applied the TV aperture, a chipset crop rect, to whatever it was given; an RTG frame fills the buffer on its own terms, so suppress it there or the board's screen shows as a sub-rect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Z3660 is a Zorro III board, so gate it on the rule Zorro III RAM already uses -- a CPU with a 32-bit address bus -- rather than a list of machine models. That fits it by default on the A3000 and A4000, and on any future profile that qualifies, so RTG needs no config step beyond installing the guest driver. Asking for it on a 24-bit-bus machine is now an error instead of a board the CPU could never reach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LinuxJedi
left a comment
There was a problem hiding this comment.
Fantastic stuff. Many thanks for adding this.
Summary
Emulates the Z3660 accelerator's FPGA RTG core as a Zorro III autoconfig
board (manufacturer 0x144B, product 1, one 128 MB window), driven by the
unmodified open-source Z3660.card Picasso96 driver. Workbench runs on
Z3660 screen modes in 8/16/32-bit with a working mouse pointer, and the
display switches cleanly between the native chipset and the RTG
framebuffer.
First RTG board for #19. The ZZ9000 (largely register-compatible with
this board) and a Copperline-native card discoverable by the bundled
AROS ROM's p96gfx HIDD are natural follow-ups on the same
infrastructure.
Enabled with:
What is emulated
0x100-0x7FF. Every register access and mailbox op is logged by name,
so the board doubles as a driver-development rig.
MONITOR_SWITCH (0 = no CIA display-switch fiddling).
so WaitVerticalSync/GetVSyncState behave like hardware.
MODE (vmode/colormode/scalemode), ORIG_RES, and the PAN op.
formats (CLUT through the captured palette, R5G6B5, R5G5B5,
B8G8R8A8) and presented by the window and the CCP capture path;
OP_CAPTUREMODE switches back to native chipset output.
doorbell write as the bus-stalling hardware does: FILLRECT, COPYRECT
(overlap-safe, 8-bit plane mask), COPYRECT_NOMASK, INVERTRECT,
RECT_TEMPLATE and RECT_PATTERN (JAM1/JAM2/COMPLEMENT + INVERSVID),
DRAWLINE (patterned, with the COMPLEMENT XOR mode the shell cursor
uses), and the planar P2C/P2D blits that render icons and window
gadget imagery.
ride the sprite ops and are composited over the output at present
time, never touching VRAM -- like the real output-stage overlay.
Semantics follow the board's ARM firmware (z3660-firmware, GPL-3.0
like Copperline), including its quirks: the GFXData pitch field is
longwords for fill/copy/invert/line but raw bytes for
template/pattern, colours ride in guest byte order (the little-endian
ARM reads the 68k values byteswapped), and minterms are the firmware's
0-15 enum, not raw blitter bytes.
Not implemented yet (logged when hit): the ACC_OP surface ops, exotic
minterms, screen-split, overlays, and the video-capture path. RTG
frames are currently scaled into the Amiga-sized presentation texture;
pixel-perfect presentation is a follow-up.
Test plan
pixel formats, pan, each blit-op family, the planar decodes, and the
sprite compositing cycle (show/upload/colour/move/hide).
and 32-bit with the real Z3660.card 1.06 built from source; window
dragging, shell text and cursor, icons, and gadget imagery render;
ScreenMode test-cycle and DISPLAYCHAIN=YES switch RTG <-> AGA both
ways. Headless: deterministic boots driving the driver via the
serial shell with CCP screenshots.
Generated with Claude Code