Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

MSX V9938 Command Engine: VRAM Operations, CE State, and CPU Contention

Model the MSX2 V9938 command engine through its register set, asynchronous VRAM work, CE status, and CPU-visible contention and completion.

The Yamaha V9938 added an important kind of work to the MSX2 video system: commands that operate on video memory without the Z80 manually moving every pixel. The command engine can search, fill, move, and logically combine regions of VRAM. This is not a modern GPU command processor with a deep independent queue. It is a small, register-programmed engine that shares a tightly constrained memory system with the display and CPU.

For software, the attraction is obvious: a block move or fill can replace a loop of port writes. For an emulator, the danger is equally clear: completing the entire command instantly makes pixels appear too early, erases CPU-visible contention, and can produce the right final screen at the wrong time. Command launch, progress, status, and VRAM access are all part of the observable VDP behavior.

The command register interface

The V9938 command parameters live in a group of video registers. They describe source and destination coordinates, dimensions, colors, logical operation, direction, and command selection. The command register starts the operation after software has prepared the other fields. The chip’s status register exposes command execution state, including a command-executing indication.

The V9938 technical data book documents operations such as high-speed VRAM-to-VRAM move (HMMM), logical move (LMMM), fill, and line drawing, alongside search and transfer-oriented commands. Each operation has its own interpretation of coordinates, dimensions, direction, and color or logical-operation bits. A decoder should dispatch by command class rather than apply one generic rectangular-copy routine to all commands.

Register preparation is a transaction from software’s point of view. If the command starts before every parameter has been written, the engine sees the currently programmed values. An emulator that buffers register writes and snapshots them only at some later host frame boundary may accidentally apply values the guest wrote after launch. Capture the command’s relevant parameters at the hardware-defined start event.

VRAM is not a host byte array

The V9938’s VRAM address space and screen modes determine how coordinates map to bits, bytes, and pixels. The same coordinates do not necessarily map to the same physical byte progression across all modes. The engine’s command semantics define which areas it can access and how it interprets a pixel or byte. Avoid applying a single linear y-times-width-plus-x transform unless the specific command/mode contract proves it correct.

The VDP can also be busy with display fetches and CPU port accesses. Slot arbitration determines when an operation reads or writes VRAM. A host copy loop that mutates an array without advancing the emulated timeline can expose destination pixels to the display earlier than the real device would. Even when a high-level renderer draws the final image, the internal VRAM state should advance in a way that preserves CPU reads and writes.

The CPU can access VDP memory using control and data ports. A command engine operation changes that memory while the processor may be polling status or issuing other writes. Model the coordination rules: whether another command can begin, how a data-port write interacts with an active command, whether the VDP reports busy, and when a status read clears or preserves flags. Consult the Yamaha data book and the selected machine’s VDP behavior, not just an emulator’s convenient implementation shortcut.

Direction and logical operations

A move needs explicit traversal direction because source and destination rectangles may overlap. If the engine copies forward over overlapping regions, pixels written early can become later source pixels. The command’s direction flags exist to establish the intended traversal order. Test both left-to-right and right-to-left cases, plus vertical overlap, instead of assuming that the host language’s generic memory-copy function exactly represents all pixel-mode behaviors.

Logical operations combine a source value with destination or color data. They should be applied at the documented pixel width and through the active mode’s encoding. Masking to a host integer width too early can corrupt packed pixels or preserve unused bits incorrectly. A useful trace reports source and destination VRAM addresses, pixel coordinates, command mode, raster/format, and the resulting value.

Command dimensions also need boundary tests. Zero width or height may have special encoding or be interpreted as a maximal count in some hardware command families; do not guess this from other VDPs. Validate the V9938 register definition and preserve the documented range in the command-specific conversion. Include right and bottom edge cases at the active screen dimensions and VRAM wrap boundaries.

Status and asynchronous completion

The command-executing state bit lets software wait for an operation to finish. This makes completion guest-visible. A command that draws the correct rectangle but clears busy one scanline too early can change program behavior, particularly if software uses completion to start a second command or reuse a buffer.

Represent an active command with the captured parameters, current pixel/byte position, remaining work, bus wait state, and pending status/interrupt effects. Schedule progress on the VDP timeline. A simpler emulator can process work in chunks, but chunks must have deterministic boundaries and cannot jump across CPU-visible status observations. The best fidelity/performance tradeoff depends on the intended system compatibility target and should be documented.

openMSX’s VDP implementation is a valuable source for how an established emulator separates command-engine behavior from other VDP work. Treat source code as corroborating implementation evidence, not as a substitute for Yamaha’s device manual. If two implementations disagree, return to the technical data book and construct a minimal program that distinguishes the hypotheses.

A testable command state

This Python fragment represents a small reference model for busy state and bounded progress. It is not a V9938 register decoder:

from dataclasses import dataclass


@dataclass
class Command:
    total_units: int
    completed_units: int = 0
    busy: bool = True

    def advance(self, units):
        if units < 0:
            raise ValueError("progress cannot move backward")
        if not self.busy:
            return
        self.completed_units = min(
            self.total_units, self.completed_units + units
        )
        self.busy = self.completed_units < self.total_units

Map units to the command’s documented work granularity and VDP scheduling, and test whether status changes at the required point. A production command engine must also model per-mode address mapping, direction, logical operations, register writes, screen fetch interference, and command-specific end conditions.

Verification workflow

First verify command-register decoding and status transitions with commands that do no visible rendering or have a tiny known effect. Then test each command family with one-pixel, one-row, full-width, reversed-direction, and overlapping rectangles. For logical operations, use a truth table covering zero and one source/destination bits. For search commands, test match at the first pixel, last pixel, no match, and wrap or end behavior defined by the manual.

Record VDP register writes, launch cycle, active mode, VRAM reads and writes, progress position, CPU port accesses, command status reads, and completion cycle. A debugger should show both raw registers and interpreted parameters. That makes a stuck busy bit distinguishable from a command that is still waiting for VRAM access.

Integration tests should include display fetches during command execution, a CPU status poll loop, and a second command attempted before the first completes. Test save-state restoration in the middle of a long fill and a right-to-left overlapping move. Compare final VRAM bytes as well as raster output; the display image alone will not reveal whether a future CPU read sees the wrong intermediate memory.

Acceptance criteria

A faithful V9938 command engine reports when a command starts, makes progress through the documented VRAM address model, observes command-specific direction and Boolean operations, and exposes busy/completion timing to the CPU. It arbitrates or otherwise models access consistently with the VDP timing target. It never equates “same final rectangle” with “same hardware behavior.”

The command engine is best understood as a register-driven coprocessor inside a video device, not a host graphics shortcut. Preserve the distinction between command parameters, VRAM storage, scanout, CPU accesses, and completion status. That structure makes both software compatibility and emulator debugging substantially more rigorous.

Keep CPU port state independent

The VDP’s control-port write sequence commonly carries both a register number and register data, and an interrupted or interleaved sequence can change how a later byte is interpreted. Preserve the control-port latch and pending first byte alongside the VDP register array. A command engine may be busy while the Z80 still writes a control byte or polls status; applying a status read as if it were a neutral host query can disrupt a guest-visible access sequence.

The V9938 also exposes status registers with different meanings. Command-executing state, collision or sprite status, and raster-related information should not collapse into one generic busy flag. A debugger should show which status selector was active, the raw status byte, the side effects of the read, and the emulated cycle. This is especially valuable when a program uses repeated polling and a command finishes between two reads.

Finally, test the V9938 in multiple display modes. Command coordinate interpretation depends on the active screen format and its pixels-per-byte layout. Use a tiny VRAM fixture with explicit address-to-coordinate expectations for every supported mode, and assert both the command’s memory result and subsequent display decode. The mode matrix catches a linear-address assumption that a single bitmap test will never expose.

Related:

Sources:

Comments