Atari 2600 Cuts Video Memory to 39 Bits, Forcing Real-Time Beam Sync

Racing the Beam Explained - Atari 2600 CPU vs. CRT Television — Retro Game Mechanics Explained

Retro Game Mechanics Explained

Because the console lacks dedicated framebuffers and coordinate registers, programmers must tally individual CPU machine cycles against the CRT electron beam to construct images scanline by scanline.

AI-assisted summary of Racing the Beam Explained - Atari 2600 CPU vs. CRT Television — original source. Check the original for full context.

Source published:

Key ideas

  • The Atari 2600 operates with virtually no conventional video framebuffer, maintaining only 39 bits of active graphical data at any given scanline moment across its playfield and sprite registers. (0:00)
  • Because the MOS 6507 CPU and the Television Interface Adapter (TIA) share a master clock, exactly one CPU machine cycle corresponds to three horizontal screen color clocks, creating a strict 76-cycle budget per scanline. (7:30)
  • The TIA lacks dedicated X and Y coordinate hardware registers, forcing software to rely on cycle-timed writes to strobe registers and fine-motion registers (HMPx/HMOVE) to position graphics horizontally. (12:02)
  • Large graphics such as 48-pixel-wide sprites bypass the 6507's three-register limit by exploiting the TIA's vertical delay registers (VDELP0 and VDELP1) as hardware holding buffers to cycle six bytes across a scanline. (33:35)

Detailed notes

## Background Published on YouTube by Retro Game Mechanics Explained on June 17, 2021, this video examines how the Atari 2600 displays graphics on a television screen despite having virtually no dedicated video graphics memory. Drawing on the video's description, the presentation details the low-level technical mechanics of how the console's microprocessor and display adapter generate images in real time.

## TL;DR Because the Atari 2600 contains only 39 bits of graphical register data and completely lacks a framebuffer or coordinate registers, the CPU must manually synchronize with the television's electron beam on a line-by-line basis. Developers positioned objects by counting CPU machine cycles from horizontal blanking intervals, combining coarse reset strobes with fine horizontal motion registers. Through detailed breakdowns of *E.T. the Extra-Terrestrial* and *Q*bert*, the video demonstrates how software exploited internal delay latches like VDEL to assemble 48-pixel sprites and rewrote playfield registers mid-scanline to create complex asymmetrical color layouts.

## Atmosphere & style The presentation carries a calm, meticulous, and deeply analytical tone, resembling an advanced computer engineering lecture that demystifies early computing hurdles. The narrator's delivery is measured and instructional, treating the platform's severe hardware constraints not as design flaws, but as fascinating puzzles solved through ingenious timing exploits. Paired with step-by-step register diagrams and cycle-by-cycle execution timelines, the experience gives viewers an appreciative, immersive look into the razor-thin margin of error retro programmers faced.

## Bulletin - [00:00](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=0s) The Atari 2600 operates with virtually no conventional video framebuffer, maintaining only 39 bits of active graphical data at any given scanline moment across its playfield and sprite registers. - [07:30](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=450s) Because the MOS 6507 CPU and the Television Interface Adapter (TIA) share a master clock, exactly one CPU machine cycle corresponds to three horizontal screen color clocks, creating a strict 76-cycle budget per scanline. - [12:02](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=722s) The TIA lacks dedicated X and Y coordinate hardware registers, forcing software to rely on cycle-timed writes to strobe registers and fine-motion registers (HMPx/HMOVE) to position graphics horizontally. - [33:35](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2015s) Large graphics such as 48-pixel-wide sprites bypass the 6507's three-register limit by exploiting the TIA's vertical delay registers (VDELP0 and VDELP1) as hardware holding buffers to cycle six bytes across a scanline.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=0s) Instead of storing thousands of bits in a multi-line video buffer, the Atari 2600 holds only 39 bits of graphic data at any instant: 20 for the playfield, 8 per player sprite, and 1 per missile and ball sprite. - [02:10](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=130s) The 160-pixel playfield is grouped into 40 blocks of 4 pixels each, with the left and right halves sharing the same 20-bit register footprint via automatic mirroring or repeating. - [04:45](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=285s) Playfield data is split across three registers (PF0, PF1, and PF2) that render in alternating bit-endian order, requiring precise timing when constructing asymmetrical displays. - [07:30](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=450s) At 76 CPU cycles per scanline and 3 color clocks per cycle, writing an asymmetrical playfield consumes 18 cycles solely on zero-page store instructions, severely restricting processor time for game logic. - [10:31](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=631s) Because sprite graphics only exist for one scanline at a time, sprites span the entire height of the screen as vertical bars unless actively zeroed out or updated line by line. - [12:02](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=722s) Horizontal positioning requires strobing reset registers like RESP0 at precise cycle counts relative to H-blank, taking advantage of a known internal hardware delay. - [15:40](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=940s) The Atari 2600 contains no vertical sync or blanking hardware counters, requiring software to manually assert VSYNC for three scanlines and manage VBLANK intervals. - [18:19](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1099s) Because raw CPU cycle timing only provides a coarse 3-pixel placement resolution, fine horizontal alignment relies on the HMPx registers and HMOVE to shift positions from -7 to +8 pixels. - [22:38](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1358s) Games can save CPU cycles during multi-line rendering because TIA registers retain their values across scanlines until explicitly overwritten. - [24:26](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1466s) Constructing a seamless 48-pixel wide sprite requires interleaving two player sprites set to triple-copy mode, offset horizontally by 8 pixels. - [29:18](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1758s) Writing six distinct bytes across a single scanline exceeds the 6507 processor's three internal data registers (A, X, and Y), creating a beam-racing bottleneck. - [33:35](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2015s) Enabling VDELP0 and VDELP1 turns the TIA's internal delay holding latches into a temporary staging register, allowing six bytes to be loaded and committed in real time. - [36:26](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2186s) Mid-scanline writes to the single COLUPF register allow games like Q*bert to display multiple playfield colors on the same horizontal raster line. - [37:13](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2233s) Mid-scanline modifications to playfield registers like PF2 maintain proper cube proportions in Q*bert, but introduce visible artifacts such as a central zig-zag seam and inverted shading.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=0s) The Concept of "Racing the Beam" and the 39-Bit Frame

The Atari 2600 represents an extraordinary paradox of early computing: its internal hardware was exceptionally primitive, to the point where making functional software was an astonishing feat. As the narrator observes, “The biggest reason why this is such a commonly stated fact is that the console has basically no video memory.” Because dedicated video memory was nonexistent, the exact same microprocessor tasked with processing game rules and user input was also forced to dictate line-by-line rendering in real time. Everything had to align precisely with the cathode-ray tube's scanning electron beam as it swept across the phosphor screen, originating the legendary engineering phrase "racing the beam."

To grasp how minimal this setup is, consider the total bit count required to generate an active frame on an NTSC television screen. While an NTSC display outputs 160 horizontal pixels by 192 vertical lines (with up to 30 additional lines sitting in overscan), and accounts for a background, two player sprites, two missile sprites, and one ball sprite at a 1-bit monochrome bit-depth, intuitive calculations fall wildly short. A conventional framebuffer calculation multiplying 160 by 222 lines plus individual sprite tables yields roughly 35,857 bits. In reality, the Atari 2600 holds a mere 39 bits of graphic data at any given moment:

- **20 bits** allocated to the background layer (formally documented as the *playfield*) - **8 bits** for Player 0 sprite graphics - **8 bits** for Player 1 sprite graphics - **1 bit** for Missile 0 - **1 bit** for Missile 1 - **1 bit** for the Ball sprite

```mermaid flowchart TD accTitle: Atari 2600 39-Bit Scanline Memory Distribution accDescr: Diagram illustrating how the 39 bits of active graphic memory are divided between playfield and sprites.

subgraph VideoMemory["Total Video Memory: 39 Bits"] PF["Playfield: 20 bits"] P0["Player 0: 8 bits"] P1["Player 1: 8 bits"] M0["Missile 0: 1 bit"] M1["Missile 1: 1 bit"] BL["Ball: 1 bit"] end

PF --> Screen["Rendered Active Scanline"] P0 --> Screen P1 --> Screen M0 --> Screen M1 --> Screen BL --> Screen ```

[02:10](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=130s) Playfield Architecture and the Shared 20 Bits

The console reaches its full 160-pixel horizontal canvas by grouping horizontal pixels into low-resolution playfield "blocks." Rather than offering 160 individually addressable playfield bits, each scanline contains only 40 distinct blocks, each spanning 4 adjacent color pixels. Halving this across the screen reveals where the 20-bit register limit originates: the left half and right half of every scanline share the exact same 20-bit memory footprint. Built-in hardware modes allow developers to mirror or repeat those 20 bits across the right half automatically, a strategy heavily relied upon across the system's catalog.

```mermaid flowchart LR accTitle: Playfield Scanline Division and Block Resolution accDescr: Block structure of an Atari 2600 playfield line showing left and right half resolution.

subgraph LeftHalf["Left Screen Half (80 Pixels)"] LBlocks["20 Playfield Blocks (4 Pixels Each)"] end

subgraph RightHalf["Right Screen Half (80 Pixels)"] RBlocks["20 Playfield Blocks (4 Pixels Each)"] end

LeftHalf -->|"Repeated or Mirrored"| RightHalf ```

Creating asymmetrical layouts required programmers to perform a hair-raising trick mid-scanline. The CPU had to push pixel bits for the left screen half into memory right as the electron beam prepared to scan it, and then, within the sliver of time it took the beam to cross the screen, overwrite those same registers with completely new values before the beam hit the right side. Straying outside this tiny execution window broke the display: “If you write the second half of the screen's data too quickly, the first half gets corrupted because the beam is still drawing that half.” Conversely, writing too late corrupted the right side because the beam had already begun scanning it. Because this 20-bit pool was shared across all vertical scanlines as well as horizontally, this exact routine had to run continuously for every single scanline down the screen.

[04:45](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=285s) Register Mapping: PF0, PF1, and PF2

The 20 bits defining half of a playfield scanline are packed into three dedicated hardware registers: `PF0`, `PF1`, and `PF2`. Together they total 24 bits, leaving 4 bits unused. The hardware accesses these registers in an alternating bit-endian pattern that complicates layout code:

```mermaid flowchart LR accTitle: Playfield Register Bit Rendering Order accDescr: Sequence and bit orientation of PF0, PF1, and PF2 registers across 20 blocks.

subgraph Order["Playfield Bit Sequence (Left Half: 20 Blocks)"] direction LR PF0["PF0 (Upper 4 Bits)<br/>LSB to MSB (Backwards)"] PF1["PF1 (8 Bits)<br/>MSB to LSB (Forwards)"] PF2["PF2 (8 Bits)<br/>LSB to MSB (Backwards)"] end

PF0 --> PF1 --> PF2 ```

- **`PF0` (4 blocks):** Only the upper 4 bits are read (the lower 4 bits are discarded). These bits render backwards, reading from least significant bit (LSB) to most significant bit (MSB). - **`PF1` (8 blocks):** All 8 bits are read forwards, progressing from most significant bit (MSB) to least significant bit (LSB). - **`PF2` (8 blocks):** All 8 bits are read backwards, moving from least significant bit (LSB) to most significant bit (MSB).

Once the first 20 blocks conclude, the console determines the second half based on mode flags. In repeat mode, the sequence runs identical to the first half (`PF0` $\to$ `PF1` $\to$ `PF2`). In mirror mode, the entire 20-bit sequence is drawn in reverse order to balance the line symmetrically. When aiming for a completely unique 40-block line, developers had to sequentially refresh each register while avoiding drawing collisions: `PF0`, `PF1`, and `PF2` had to be rewritten at precise intervals after their corresponding left-hand blocks finished drawing, but strictly before the beam arrived to read them for the right-hand blocks. Writing while a block was being actively rendered caused visual garbage on screen.

[07:30](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=450s) Machine Cycles, Clock Timing, and the Real Cost of Asymmetry

The feasibility of updating these registers comes down to the tight relationship between the console's internal processor and the television's electron beam. The Atari 2600 is driven by a MOS 6507 CPU clocked at approximately 1.19 MHz. Because the visual hardware derives its timing from the same master clock, the math locks into a fixed ratio: exactly one CPU machine cycle elapses for every 3 horizontal screen pixels drawn by the beam.

| Metric | Machine Cycles | Equivalent Pixels / Description | |:--- |:--- |:--- | | **Total Line Duration** | 76 cycles | Includes active display and Horizontal Blanking (H-Blank) | | **1 CPU Machine Cycle** | 1 cycle | Exactly 3 horizontal pixels drawn | | **Zero-Page Memory Write** | 3 cycles | 9 pixels (equivalent to $2 \frac{1}{4}$ playfield blocks) | | **Latest Initial `PF0` Write** | 19 cycles | Maximum cycles after H-Blank begins for the first playfield write |

```mermaid flowchart LR accTitle: Scanline CPU Cycle Budget for Playfield Updates accDescr: Timing timeline showing machine cycle consumption across a single 76-cycle scanline.

subgraph LineBudget["Single Scanline: 76 Machine Cycles Total"] direction LR HB["H-Blank to PF0: 19c"] W1["Writes (3c each): 18c"] Gaps["Execution Windows / Gaps: 28c"] Tail["Final Slack to Next H-Blank: 11c"] end

HB --> W1 --> Gaps --> Tail ```

Forcing an asymmetrical playfield required six distinct writes per scanline (three registers written twice). At 3 machine cycles per zero-page store instruction, the writes alone ate up 18 of the line's 76 cycles: - Write `PF0` (Left) at cycle 19 (takes 3 cycles) - 3-cycle gap, then write `PF1` (Left) (takes 3 cycles) - Execution gaps of 7, 8, 2, and 8 cycles between the subsequent writes for `PF2` (Left), `PF0` (Right), `PF1` (Right), and `PF2` (Right) - An 11-cycle buffer remaining after the final `PF2` write before the next line's H-blank begins

Crucially, those 18 cycles accounted *only* for transferring bytes into the registers—they did not include retrieving data from ROM or RAM, calculating sprite positioning, or running actual game logic. Consequently, fully custom backgrounds were rarely practical, leading developers to rely heavily on mirrored or repeated layouts to keep CPU time free for other tasks.

[10:31](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=631s) Sprite Hardware Limitations and Repositioning

Sprited elements operate under constraints just as severe as the playfield. The hardware provides two player sprites, two missile sprites, and a single ball sprite. Each player sprite possesses only 8 bits of graphic data per scanline, while the missiles and the ball possess a single bit (on or off). Because the system only holds graphic values for a single scanline, any vertical dimension requires manual intervention: “In fact, the only way to disable a player sprite is to just set its graphics to all zeros, so you will need to update it on the fly anyway, unless your player sprite is going to be a big vertical column spanning the entire screen.”

Built-in hardware registers allow player and missile sprites to be scaled up or duplicated across a single scanline. Even then, the backing storage remains locked at 8 bits per player and 1 bit per missile. Skilled developers bypassed this limit by overwriting the 8-bit player register during the microsecond gap between the primary sprite and its hardware duplicate, synthesizing the illusion of two completely unique sprites on a single line out of a single register.

However, placing a sprite on screen exposes the system's most counterintuitive hurdle. If a developer wants to render an $8 \times 8$ graphic for Player 0 at line 40 from the top and 60 pixels from the left, standard coordinates do not exist. There are no X and Y position registers where a programmer can simply write `60` and `40`. Positioning sprites requires an entirely different, timing-based approach.

[12:02](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=722s) Horizontal Sprite Positioning via Cycle Counting and WSYNC

Positioning sprites on the Atari 2600 requires manual calculation because the Television Interface Adapter (TIA) lacks dedicated X and Y coordinate registers. To position an object horizontally, programmers must interact with one of five reset sprite strobe registers—such as `RESP0`—where any write triggers the sprite's horizontal origin to latch to the scanning beam's current location. Due to internal hardware delay, the sprite actually appears slightly to the right of the beam's position when the store instruction concludes; for an unstretched player sprite, this offset is exactly five pixels. Therefore, placing a sprite 60 pixels from the screen's left edge requires timing the CPU so that the write to the reset register finishes precisely as the beam scans the 55th visible pixel.

Determining the scanning beam's location at any given instant presents a fundamental challenge. Retro Game Mechanics Explained emphasizes that “There is no register to read the current position of the beam.” Instead, developers must synchronize the CPU with the beam at a fixed point and tally machine cycles for every subsequent instruction. Writing to the special `WSYNC` register halts the CPU until the beam finishes the active scanline, guaranteeing that the following instruction executes at the start of horizontal blanking (H-blank). H-blank spans a duration equivalent to 68 color clocks, or "pixels," before the visible scanline begins. Factoring in the desired 55-pixel visible offset yields 123 total pixels. Since one CPU machine cycle corresponds to three color clocks, 123 pixels equates to 41 machine cycles. Subtracting the 3 cycles consumed by the final `STA RESP0` instruction leaves 38 cycles that must be filled.

A programmer can spend these 38 cycles loading sprite graphics and idling. Loading graphic data via an immediate `LDA` (2 cycles) and an `STA` to the graphics register (3 cycles) absorbs 5 cycles, leaving 33 cycles remaining. Because the 6502 architecture lacks a single-cycle instruction, this exact timing is achieved by pairing a dummy 3-cycle `STA` instruction with fifteen 2-cycle `NOP` instructions. After storing to `RESP0` and following up with an `STA WSYNC`, the sprite's horizontal coordinate remains persistent across future scanlines until another reset strobe occurs. Consequently, rendering the remaining seven scanlines of an eight-line sprite only demands loading graphics, storing them to the graphics register, and strobing `WSYNC`.

```mermaid flowchart TD accTitle: Horizontal Alignment Cycle Budget accDescr: Breakdown of CPU machine cycles required to position a sprite at pixel 60 from H-blank.

subgraph Sync["Synchronization Phase"] W["STA WSYNC<br/>(Halts CPU until line end)"] --> HB["Start of H-Blank<br/>(Target: 123 pixels / 41 cycles)"] end

subgraph Timing["Cycle-Burning Work (38 Cycles)"] HB --> G["LDA #imm + STA GRP0<br/>(Graphics load: 5 cycles)"] G --> D["Dummy STA (3 cycles)<br/>+ 15 NOPs (30 cycles)"] end

subgraph Latch["Strobe Phase"] D --> R["STA RESP0 (3 cycles)<br/>(Latches position at pixel 55)"] R --> S["Hardware Delay (+5 px)<br/>Sprite positioned at visible pixel 60"] end ```

- Horizontal reset registers: five strobe registers (`RESP0` through `RESP4`), one for each sprite object. - Unstretched player sprite hardware delay: 5 color clocks (pixels) after instruction execution. - H-blank duration: 68 color clocks before active video display. - Cycle-to-pixel ratio: 1 CPU machine cycle equals 3 TIA color clocks/pixels. - Timing breakdown for 60-pixel placement: 68 H-blank pixels + 55 pixel offset = 123 pixels = 41 machine cycles. - Cycle allocation: 5 cycles (graphic load) + 3 cycles (dummy `STA`) + 30 cycles (15 `NOP`s) + 3 cycles (`STA RESP0`) = 41 total machine cycles.

---

[15:40](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=940s) Vertical Timing, VSYNC, and the VBLANK Period

While horizontal alignment can be tied directly to hardware scanlines via `WSYNC`, vertical alignment presents a far more bare-bones environment. Retro Game Mechanics Explained explains: “The Atari actually has no hardware for controlling vertical syncing or blanking at all; that has to be done in the game's code as well.” Television receivers depend on a vertical synchronization signal—a brief period where the video voltage drops into a "darker than black" negative range—to recognize the boundary between frames. On the Atari 2600, software must manually turn this signal on and off using the `VSYNC` register. To maintain a stable image on standard displays, code must assert `VSYNC` for precisely three scanlines and then clear it back to zero, typically coordinating each transition by strobing `WSYNC`.

Immediately following the vertical sync pulses, the vertical blanking (V-blank) interval begins. The programmer enables blanking by writing a 1 to the `VBLANK` register, disabling video output while the CRT's electron beam sweeps from the bottom of the screen back up to the top. Standard NTSC format allocates 37 scanlines to V-blank (compared to 45 lines on PAL consoles). Because the electron beam is inactive during this interval, game engines are freed from rendering duties. This makes V-blank the primary processing window for game logic, physics, and controller input routines.

If a game has no logic to run, it simply burns these lines by writing to `WSYNC` repeatedly. Displaying a sprite 40 scanlines below the top of the visible screen requires waiting out the 37 lines of NTSC V-blank plus the 40 visible lines, totaling 77 scanlines after vertical sync concludes. Once the initial 37 lines elapse, the game clears `VBLANK` by writing zero, counts out 40 more `WSYNC` strobes, and finally executes the cycle-counted horizontal rendering routines prepared for the graphic data.

- `VSYNC` signal: held active for exactly 3 scanlines to establish a standard frame transition. - NTSC vs. PAL vertical blanking: NTSC uses 37 V-blank lines; PAL systems use 45 V-blank lines. - Total pre-sprite wait: 37 blanking lines + 40 visible lines = 77 scanlines before sprite data is drawn. - Hardware absence: no vertical counters or automatic blanking timers exist in the TIA silicon.

---

[18:19](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1099s) Coarse and Fine Horizontal Positioning in E.T.

Real-world games rarely rely solely on raw delay loops for every coordinate because counting CPU cycles only provides a coarse positioning resolution. Since a single machine cycle takes three color clocks, strobing `RESPx` alone can only place an object in increments of three pixels. Retro Game Mechanics Explained notes: “To locate sprites horizontally down to the pixel, you need to use the horizontal movement registers.” The TIA solves this by dividing horizontal placement into coarse positioning (strobe registers) and fine adjustments (horizontal motion registers `HMP0`–`HMP4` and `HMOVE`).

```mermaid flowchart LR accTitle: Coarse and Fine Horizontal Positioning Architecture accDescr: Flow diagram showing how coarse positioning loops and fine motion values combine via HMOVE.

subgraph Coarse["Coarse Timing"] A["WSYNC Strobe"] --> B["Inner Delay Loop<br/>(5 cycles / 15 px steps)"] B --> C["RESPx Strobe<br/>(Sets rough position)"] end

subgraph Fine["Fine Offset"] D["Load Nibble to HMPx<br/>(-7 to +8 pixel shift)"] --> E["Strobe HMOVE<br/>(Applies fine offset to all sprites)"] end

C -.-> E ```

The title screen of *E.T. the Extra-Terrestrial* provides a clear demonstration of this dual mechanism. The title graphic displays "E.T." where the "E" is Player 0, the "T" is Player 1, and the two trailing periods are Missile 0 and Missile 1. To position all elements, a unified setup loop iterates five times (covering all sprites, while disabling the ball). Each pass begins with a `WSYNC` so that one sprite is configured per scanline. Inside the loop, the CPU loads a byte from ROM: the upper nibble supplies the fine positioning value written to the corresponding `HMPx` register, while the lower nibble acts as a loop counter for a small cycle-wasting inner loop. Each pass of this inner loop consumes 5 CPU cycles, advancing the beam by 15 color clocks. Because the fine motion registers accommodate a range of 16 pixels (from 7 pixels left to 8 pixels right), this 15-pixel coarse step size is completely bridged.

For the capital "E", the game retrieves the byte `$52` from ROM. The upper nibble (`$5`) commands a fine shift of 5 pixels to the left. The lower nibble (`$2`) causes the inner branch loop to execute twice. By the time the code strobes `RESP0`, 36 machine cycles have elapsed since the start of H-blank. Multiplying 36 cycles by 3 yields 108 pixels; subtracting the 68 pixels of H-blank and adding a 6-pixel delay (offset by one extra pixel because the sprite is stretched) establishes a coarse position at pixel 46. Once all five sprites receive their coarse strobes and fine values, a single strobe to `HMOVE` applies the queued offsets simultaneously. This shifts Player 0 leftward by 5 pixels, placing the "E" at its final destination: exactly 41 pixels from the left border.

- Resolution limit: direct `RESPx` writes have a 3-pixel granularity due to the 1:3 CPU-to-clock ratio. - Fine adjustment limits: `HMPx` values shift sprites from -7 pixels (left) to +8 pixels (right). - ROM data packing: byte `$52` encodes both fine displacement (upper `$5`) and loop count (lower `$2`). - Delay scaling: each inner loop iteration costs 5 machine cycles (15 color clocks/pixels). - "E" sprite coarse calculation: 36 cycles × 3 = 108 pixels; 108 − 68 H-blank + 6 delay = 46 pixels. - Final position: 46 coarse pixels − 5 fine pixels (`HMOVE`) = 41 pixels from screen edge.

---

[22:38](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1358s) Rendering the Title Graphics and Multi-Line Units

Once positioning registers and horizontal offsets are latched, execution shifts to the scanline-by-scanline rendering loop. On the scanline immediately preceding the text display, an initialization routine establishes sprite properties: selecting colors, disabling horizontal reflection, and defining sizes. The player sprites forming the "E" and "T" are configured to stretch at 4 pixels per bit, while the missiles forming the periods are set to a width of 2 pixels.

The main text block is 16 units high, meaning the display loop runs 16 times, pulling bitmap bytes from ROM and storing them directly into the graphic registers (`GRP0` and `GRP1`). The missile registers are updated only during the final two iterations to render the periods at the base of the letters. Every unit of graphic text is two scanlines tall, resulting in "pixels" that measure 4 clocks wide by 2 scanlines high. To maintain this aspect ratio, the loop contains two separate writes to `WSYNC`. Retro Game Mechanics Explained highlights an efficient aspect of the TIA architecture: “Recall that if nothing changes from one scanline to the next, no code needs to be written.” Because sprite graphics persist in TIA registers across scanlines, the CPU spends the second scanline doing virtually nothing—executing only a single `DEX` instruction to decrement the loop counter before waiting for the next line.

After completing all 16 iterations, the routine yields CPU time to downstream display routines. These subsequent routines handle the remainder of the frame, including drawing the large rendering of E.T.'s face located further down the screen—a graphic spanning 48 pixels in width and 40 scanlines in height, assembled entirely using the two player sprites.

- Graphic dimensions: player letters stretched to 4 color clocks per bit; missile dots set to 2 pixels wide. - Virtual pixel aspect: each letter unit covers 4 horizontal pixels and 2 vertical scanlines. - Loop mechanics: 16 iterations total, containing two `WSYNC` strobes per loop pass. - Inactive scanline economy: second scanline executes only a 2-cycle `DEX` instruction while the TIA re-displays latched data. - Subsequent frame element: E.T.'s face measures 48×40 pixels, multiplexing only the two hardware player sprites.

[24:26](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1466s) Setting Up a 48-Pixel Sprite Across Three Scanlines

Displaying a large sprite on the Atari 2600 requires chaining hardware tricks together. A 48-pixel wide graphic can be constructed by configuring the TIA to repeat each player sprite three times per scanline with 8-pixel gaps between copies. By offsetting player sprite 0 and player sprite 1 by exactly 8 pixels, the two sets of three interleaved 8-pixel segments cover the entire 48-pixel span seamlessly. The full sprite image comprises 6 columns across 40 rows (a $6 \times 40$ array of bytes), where each byte defines an $8 \times 1$ pixel strip stored in a ROM lookup table.

Because of the heavy register preparation involved, the execution routine must start three scanlines ahead of the actual render. On the first preparatory scanline, sizing, reflection, and positioning registers are primed:

```mermaid flowchart LR accTitle: Two-Player Interleaving for 48-Pixel Sprite accDescr: Interleaving copies of Player 0 and Player 1 across a 48-pixel span.

subgraph Combined["Combined 48-Pixel Span"] P0_A["P0 (Byte A)"] --> P1_B["P1 (Byte B)"] P1_B --> P0_C["P0 (Byte C)"] P0_C --> P1_D["P1 (Byte D)"] P1_D --> P0_E["P0 (Byte E)"] P0_E --> P1_F["P1 (Byte F)"] end ```

The accumulator is loaded with immediate value `#$03`, and the Y index register is cleared with `#$00`. Reflection registers are zeroed so the sprite appears unflipped, while the sizing registers are written with `#$03` to trigger the "three copies at 8-pixel intervals" mode.

Crucially, this setup introduces the graphics delay registers, `VDELP0` and `VDELP1`: “When VDELP0 is set, the graphics for player sprite 0 won't fully go through until the graphics for player sprite 1 is written.” Similarly, when `VDELP1` is enabled, player 1's graphic is held until player 0's register receives a write. Enabling both registers simultaneously creates internal temporary holding buffers. As the narrator explains, “This is used for timing reasons--it allows an extra position value to be queued up which lets this method of writing 6 bytes worth of graphics data quickly to work at all.”

After double-writing zeroes to clear the delayed graphics registers and inserting a single 2-cycle `NOP` instruction, the CPU writes to the reset position registers (`RESP0` and `RESP1`) at cycles 39 and 42. Multiplied by 3 color clocks per cycle, minus 68 clocks for horizontal blank, plus 5 clocks of internal beam delay, this yields horizontal screen positions of 54 and 63. Because a standard 3-cycle `STA` instruction spaces the writes 9 pixels apart rather than 8, the hardware horizontal motion register (`HMM0`) must be loaded to nudge player 0 one pixel to the right (to position 55), perfectly closing the gap to an 8-pixel offset.

- **Initial Register Values**: `#$03` loaded into Accumulator; `#$00` loaded into Y index register. - **Position Calculation**: Cycle 39 $\to 39 \times 3 - 68 + 5 = 54$; Cycle 42 $\to 42 \times 3 - 68 + 5 = 63$ (an initial spacing of 9 pixels). - **HMOVE Adjustment**: `HMM0` shifts player 0 from horizontal position 54 to 55 to establish the exact 8-pixel offset.

---

[27:45](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1665s) RAM Pointer Initialization and the First Queued Byte

The second setup scanline prepares the pointers needed to retrieve graphic data from ROM into zero-page RAM. The very first instruction is an immediate write to `HMOVE`, adhering to the strict rule that horizontal motion strobes should execute right after writing to `WSYNC`. The remainder of this scanline populates six 16-bit pointers corresponding to the six vertical columns of the 48-pixel sprite. This abstraction enables the underlying draw logic to function as a general-purpose subroutine callable for any arbitrary 48-pixel image.

The third setup scanline configures the sprite color registers and initializes the loop counter to `#$27` (decimal 39), allowing a 40-scanline routine to execute. The CPU then executes a `JSR` jump to the general drawing subroutine. Before this final preparatory scanline finishes, the graphic data for the leftmost column (byte A) is loaded into `GRP0`. Because `VDELP0` is active, byte A sits latched inside the temporary buffer rather than immediately displaying. The scanline concludes by strobing `WSYNC` to halt the CPU until horizontal blanking completes, awaiting the scanline where the sprite actually becomes visible.

---

[29:18](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1758s) The Beam-Racing Bottleneck: 3 Registers vs. 6 Bytes

Once the scanline rendering loop begins, the system races against the electron beam across the 48-pixel display area. The fundamental rule of the TIA graphics registers is that the internal hardware latches data during the final machine cycle of an instruction: “So a write instruction needs to completely finish executing before the beam even begins drawing that part of the sprite.” Because the write only lands on the final cycle, an instruction may begin executing while the CRT beam is still drawing the trailing edge of a previous sprite copy. This overlap is vital because an absolute `STA` takes 3 CPU cycles (equivalent to 9 color clocks/pixels), which exceeds the 8-pixel interval between adjacent sprite copies.

Analyzing a naive approach demonstrates why unbuffered writes fail:

```mermaid flowchart TD accTitle: Data Bottleneck Flow accDescr: Sequence showing CPU register limitations when writing six bytes across one scanline.

subgraph CPU["6507 Internal Registers"] RegA["Accumulator (A)"] RegX["Index (X)"] RegY["Index (Y)"] end

subgraph Target["Required Sprite Bytes"] B1["Byte A"] B2["Byte B"] B3["Byte C"] B4["Byte D"] B5["Byte E"] B6["Byte F"] end

CPU -->|"Can only hold 3 values simultaneously"| Bottleneck{"Mid-line reload impossible<br/>without TIA delay latch"} Target --> Bottleneck ```

On a grid where instructions can only align to 3-pixel boundaries, working backwards reveals that writing bytes C, D, E, and F in direct succession would demand unbroken writes to `GRP0` and `GRP1`. However, the 6507 processor contains only three general-purpose data registers: A, X, and Y. “We can't have six write instructions in a row, especially if all six data values are different,” the narrator emphasizes. “The 6507 CPU only has three internal registers for general-use data. So it can only hold three values at a time.”

While the first two writes can easily be pulled forward into the scanline before the beam reaches the sprite area, that still leaves four bytes to update on the fly. Preloading A, X, and Y handles three of those values, but the fourth cannot be retrieved during rendering: - A ROM read consumes too many cycles to squeeze between active sprite copies. - Even offloading data to the stack pointer register (`S`) with a 2-cycle transfer instruction cannot avoid colliding with byte A as it renders. - Writing byte C directly without a delay mechanism corrupts the ongoing display of byte A.

---

[33:35](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2015s) Exploiting VDEL to Cycle Six Bytes in Real Time

Enabling `VDELP0` and `VDELP1` resolves the hardware register shortage by turning the TIA's internal holding latches into a fourth temporary staging register. Writing byte A into `GRP0` leaves it dormant in the staging register. When the CPU subsequently writes byte B into `GRP1`, two actions occur at once: byte B enters player 1's holding latch, and byte A is pushed through into player 0's active graphics register. Next, writing byte C to `GRP0` commits byte B to player 1 while caching byte C in player 0's buffer.

This delay mechanism reshuffles the execution schedule: 1. Byte A is pre-written to `GRP0` at the tail end of the preceding scanline. 2. At the start of the active scanline, byte B is written to `GRP1` (pushing byte A live), and byte C is written to `GRP0` (pushing byte B live while byte C waits). 3. With byte C safely parked in the TIA hardware latch, the CPU has enough cycle headroom to reload A, X, and Y with bytes D, E, and F via indirect ROM reads (each taking 5 cycles). 4. Because the Y index register is needed to index through the indirect pointers, it must be clobbered last, and a temporary memory byte temporarily buffers the value destined for the accumulator. 5. Bytes D, E, and F are written to their respective registers in succession. 6. A final dummy write to `GRP0` forces byte F out of the internal temporary latch and onto the screen just as the beam arrives.

After the dummy write, the scanline loop counter decrements and branches back to render the next scanline. Once all 40 lines finish, execution drops out of the loop, clearing the graphics, size, and delay registers back to zero.

---

[36:26](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2186s) Dynamic Mid-Scanline Playfield Palette Changes (Q*bert)

The playfield can be manipulated on the fly just like player sprites. In the Atari 2600 port of *Q\*bert*, the isometric pyramid is rendered via playfield graphics. The layout is noticeably asymmetric due to the single center cubes on odd-numbered rows, and visually the pyramid displays three or four colors on the same scanline—seemingly breaking the rule that the playfield possesses only a single color register (`COLUPF`).

This visual variety is accomplished by racing the beam to update color registers mid-scanline: “Just like how the graphics data for things can be changed mid-scanline, so can the colors. Only one color can be used at a time, but that one color can change over time.” While the TIA cannot output yellow and blue playfield pixels concurrently on the exact same pixel clock, the CPU can write to `COLUPF` right as the beam moves across different cube facets. By synchronizing register writes with the beam's sweep from left to right, the playfield shifts its palette from blue to yellow against a shared black background across a single horizontal scanline.

[37:13](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2233s) Mid-Scanline Playfield Updates and Visual Artifacts

The coloration of the visual elements on the display is managed through the `COLUPF` (Color-Luminance Playfield) register. In the scanline under examination, three cubes are rendered across the screen. Because the specific color values assigned to these cubes already exist in memory, the processor's task during the active scanline is simply to fetch those pre-stored values and move them directly into the appropriate hardware register at precisely timed intervals.

However, rendering these cubes correctly introduces significant architectural friction due to their unconventional dimensions. To keep the proportions accurate, the playfield registers—most notably `PF2`—must be modified in the middle of an active scanline. The narrator explains the engineering compromise required for this display trick: “This is just so that the center cube doesn't show up larger than the others--however it does result in this large zig-zag shaped gap.” By executing an exactingly timed mid-line write, the two halves of the center cube are able to render cleanly despite sharing the exact same underlying bits of data.

At the same time, relying on this mirrored playfield technique introduces distinct visual side effects beyond the jagged central seam. Because the hardware mirrors the data across the screen, the apparent light direction and shading across the three cubes end up inconsistent, serving as a direct visual footprint of the Atari 2600’s hardware constraints and raster-racing tricks.

```mermaid flowchart TD accTitle: Mid-Scanline Playfield Update Flow accDescr: Diagram showing how pre-stored color values and timed mid-scanline writes to PF2 prevent the center cube from distorting while causing visual artifacts.

subgraph Memory ["Pre-stored Data"] A["Color Values in Memory"] end

subgraph TIA ["TIA Registers"] B["COLUPF Register"] C["PF2 Register (Updated Mid-Scanline)"] end

subgraph Output ["Screen Display Output"] D["Three Balanced Cubes"] E["Zig-Zag Gap Artifact"] F["Inconsistent Cube Shading"] end

A -->|"Timed write"| B B --> D C -->|"Prevents oversized center"| D C -->|"Side effect"| E C -->|"Mirroring effect"| F ```

- The `COLUPF` register directly determines the active color of the playfield graphics on a given scanline. - Mid-scanline writes to `PF2` prevent the center cube from appearing visibly wider than the outer cubes. - A prominent zig-zag gap down the screen's vertical center is an unavoidable artifact of the mid-line register timing. - Hardware-level playfield mirroring prevents symmetrical lighting across all three rendered cubes.

---

[38:01](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2281s) Channel Outro and Community Support

Wrapping up the technical breakdown, the narrator expresses appreciation to the audience for watching the video. Viewers who wish to provide financial backing to Retro Game Mechanics Explained are directed to the channel's crowdfunding platforms on Patreon and SubscribeStar. For those unable or not wishing to pledge monetary support, the narrator emphasizes that standard engagement—watching videos to completion, leaving comments, and sharing the content with friends—is an equally valuable way to support the project. Viewers interested in further deep dives into the Atari 2600 hardware and its unique programming hurdles are encouraged to leave a comment requesting future coverage.

## Notable quotes > [02:10](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=130s) If you write the second half of the screen's data too quickly, the first half gets corrupted because the beam is still drawing that half. — Retro Game Mechanics Explained

> [15:40](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=940s) The Atari actually has no hardware for controlling vertical syncing or blanking at all; that has to be done in the game's code as well. — Retro Game Mechanics Explained

> [36:26](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=2186s) Only one color can be used at a time, but that one color can change over time. — Retro Game Mechanics Explained

> [22:38](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1358s) If nothing changes from one scanline to the next, no code needs to be written. — Retro Game Mechanics Explained

> [29:18](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1758s) A write instruction needs to completely finish executing before the beam even begins drawing that part of the sprite. — Retro Game Mechanics Explained

> [18:19](https://www.youtube.com/watch?v=sJFnWZH5FXc&t=1099s) To locate sprites horizontally down to the pixel, you need to use the horizontal movement registers. — Retro Game Mechanics Explained

## Mentioned Atari 2600, MOS 6507, Television Interface Adapter (TIA), Cathode-Ray Tube (CRT), NTSC, PAL, E.T. the Extra-Terrestrial, Q*bert, Retro Game Mechanics Explained, Patreon, SubscribeStar

## Video description How does the Atari 2600 display images to the screen with so little memory dedicated to video graphics? It's all explained right here. LINKS Twitter (updates): https://twitter.com/RetroGameMechEx Patreon (support): https://www.patreon.com/rgmechex SubscribeStar (support): https://www.subscribestar.com/rgmechex Discord (discussion): http://discord.rgmechex.com PATRONS Special thanks to everyone who makes this channel possible! Anthony Losego, Dan Salvato, F. Murmel, Jonathan Aldrich, Ange Albertini, Mark Canlas, Avi Drissman, Steven, Mike Gerow, Larry Koubiak, Tina Wuest, Owen Christensen, Buddy, Brandan Lennox, Jason Hughes, Diamond Ice, Chris Post, Cypher Signal, Rupix, krivx, Xkeeper, Walter Huf, David Spalding, Acceleration Shark, Rory Kelly, Joe Mecca, Ginger Spice, Kefen, Heptonion, leftler, Scott Beca, 19day, Michael Dragone, John Losego, Travis, Arthur Kunkle, Jordan Wright, Stephan Hennion, node1729, Andrea, Michael Cafarelli, KieferSkunk, James Church, Rad, John Kreitlow, Vladimir Kirov, Tromodolo, Zhongchao Qian, Cobalt, Kang Seonghoon, Exani Momeo, Nudelreaktor, tripper, Sten, 333Rich333, The Revd. Juli Mallett, Glenn hEADcRASH Sugden, David Mazarro, null, Ryan, Corey Ogburn, Martin Trozell, Garret Kelly, Joel Kuhn, Dan Shedd, Sembiance, Xander479, 4F Panda, David, Alec Johnson, Brian Henriquez, Travis Nellor, Zach Hugethanks, Oxygen Chen, RetroReversing.com, Ceres, Diego Santos Leão, Jeremiah, Chris Connett, Mark, Matthew, sapslaj, Bjoern Hansen, ers35, Pixy011, Daniel Bernard (ReckedCat), Lukas Kalbertodt, Vier Ladair, Bwangry, iPaq, Metlwing, David Johnson, Matt Shepard, Felix Freiberger, Sypwn, Niles Rogoff, Reflet, Eugene Bulkin, Walter Weaver, Articate, Julien Oster, Steve Losh, Samuel Stoddard, HattyJetty, Paige ? Hex, Yeero, Cruz Godar, Linh Pham, Noah Greenberg, Nick Rogers, Sean Nelson (audiohacked), Bryce, Andrew Yukhymchak, Sean Bryant, John Gabriel, BazBadger, Kyle, Master Knight DH, Tim Romero, Michael B., Eric Loewenthal, Adrian Haslinger, Proxy, Nolan Varani, Daniel Robinson, Hans Jorgensen, Daniel A.A. Pelsmaeker, Aaron, Max Gartung, Nicolas Dohrendorf, ei, Chaz Serir, derHinek, Myriad, samfu, Dominic Wehrmann, André Greubel, Urda, AA, Joseph Torres, Daniel Hug., Audio, Rodrigo Monteiro, Punchmaster, Stephen Bank, Dasterin, Matthew Yu, Alice Hartley, Marcus N., Agoaj, Nicholas Carhuff, Epsilion, bob johnson, Permian Strata, supergtt, Alex Berliner, Sean Coates, Eniplay, iWasHere, Alex, Dominic Cerquetti, Adam Streeter, Eric Romero, Clyde Shaffer, Oast, Esme, FinalQuark, Stefan Eisenreich, pagetable.com, Anthony McNamara (ekinsthepokemon), Keithius, Marius Schoof, Joe Sewell, Rafael De Los Santos, orgamecha, Brandon Stansbury, Steven Foley, ???? ????, & John Bruce!

[Source](https://www.youtube.com/watch?v=sJFnWZH5FXc)

中文简报

雅达利2600将显存削减至39位,迫使程序与电子束实时同步 由于该主机缺乏专用帧缓冲区和坐标寄存器,程序员必须对照CRT电子束精确计算CPU机器周期,以逐行构建画面。 由于雅达利2600仅包含39位的图形寄存器数据,且完全缺乏帧缓冲区或坐标寄存器,CPU必须逐行与电视机的电子束进行手动同步。开发者通过计算水平消隐间隔后的CPU机器周期,结合粗调复位选通与微调水平移动寄存器来定位物体。视频通过对《E.T.外星人》和《Q*bert》的深入剖析,展示了软件如何利用VDEL等内部延迟锁存器组装48像素的精灵图,以及如何在扫描线中途重写活动场地寄存器以构建复杂的非对称色彩布局。 雅达利2600在运行中几乎没有传统的视频帧缓冲区,其活动场地与精灵图寄存器在任一扫描线时刻仅维持39位的活动图形数据。 (0:00) 由于MOS 6507 CPU与电视接口适配器(TIA)共享主时钟,恰好一个CPU机器周期对应三个水平屏幕色彩时钟,从而在每条扫描线上构成了严格的76周期预算。 (7:30) TIA缺乏专用的硬件X和Y坐标寄存器,迫使软件必须依赖对选通寄存器和微调移动寄存器(HMPx/HMOVE)进行精确到周期的写入,来在水平方向上定位图形。 (12:02) 48像素宽的精灵图等大型图形利用TIA的垂直延迟寄存器(VDELP0和VDELP1)作为硬件暂存缓冲器,在一个扫描线周期内轮换六个字节,从而绕过了6507仅有三个寄存器的限制。 (33:35)

Reading summary

Retro Game Mechanics Explained details how the Atari 2600's lack of video memory forced developers to "race the beam," synchronizing the MOS 6507 CPU cycle-by-cycle with the CRT electron beam to construct screen graphics in real time from only 39 bits of active visual data. The 39-Bit Scanline and Playfield Architecture (0:00) While an NTSC frame buffer would conventionally require about 35,857 bits of storage, the Atari 2600 holds only 39 bits of graphic data at any instant: 20 bits for the background playfield, 8 bits each for two player sprites, and 1 bit each for two missiles and a ball. The 20-bit playfield spans 40 horizontal blocks across the screen, each 4 color pixels wide. The left and right halves share the exact same 20-bit memory footprint mapped across three hardware registers: PF0 (4 bits, read backwards), PF1 (8 bits, read forwards), and PF2 (8 bits, read backwards). Because the hardware automatically repeats or mirrors those 20 bits, rendering an asymmetric background required the CPU to rewrite all three registers in the brief interval between the beam scanning the left and right halves. Driven by a 1.19 MHz processor where one machine cycle equals exactly three color pixels, a single scanline provides only 76 cycles. Storing data to zero-page registers twice per line consumes 18 cycles solely for the write instructions, leaving minimal headroom for sprite calculations or gameplay logic. Software-Driven Positioning and Frame Synchronization (12:02) The Television Interface Adapter (TIA) contains no horizontal or vertical coordinate registers, forcing developers to calculate sprite coordinates through CPU instruction timing. Horizontal alignment is handled by strobing reset registers like RESP0, which latches a sprite's origin to the current beam position plus a 5-pixel hardware delay. By strobing WSYNC to halt the CPU until the end of a line, programmers can count machine cycles across the 68-pixel horizontal blanking period and active scanline to position objects. Because individual CPU cycles have a coarse 3-pixel granularity, fine placement requires writing to motion registers (HMP0–HMP4) and triggering HMOVE to shift graphics by -7 to +8 pixels. Vertical timing is similarly stripped of silicon automation. The console lacks hardware counters or timers for frame boundaries, requiring software to manually assert the VSYNC signal for exactly three scanlines. Developers must then track the 37 scanlines of NTSC vertical blanking (VBLANK) by issuing repeated WSYNC halts. Because the electron beam is inactive during VBLANK, this window serves as the engine's primary execution buffer for game rules, physics, and controller input. Overcoming Register Limits with Delay Latches (24:26) Constructing wide graphics exposed the limits of the 6507 CPU, which possesses only three general-purpose data registers (A, X, and Y). In the title screen of E.T., an assembly routine generates a 48-pixel-wide face sprite by interleaving copies of both player sprites across six 8-pixel ROM bytes. Because an absolute write consumes 3 cycles (9 pixels), direct sequential writes would overrun the 8-pixel spacing between adjacent segments and corrupt active screen data. Programmers resolved this register bottleneck by exploiting the TIA's graphics delay registers, VDELP0 and VDELP1. Enabling these registers turns the internal hardware latches into a staging buffer: writing data to one player register does not display immediately, but instead queues the graphic until the opposite player register receives a write. This hardware buffering allowed the CPU to preload bytes and execute indirect ROM reads mid-scanline, cycling all six bytes into the display registers in real time without overwriting pixels currently being traced by the beam. Dynamic Palette Swapping and Geometric Artifacts (36:26) Programmers applied the same mid-scanline racing techniques to hardware color registers to expand the system's aesthetic palette. In the Atari 2600 port of Q*bert, the isometric pyramid features multiple colors per scanline despite the playfield possessing only a single color register (COLUPF). By synchronizing CPU writes to COLUPF precisely as the scanning beam crossed between adjacent cube facets, the game altered the active palette mid-line from blue to yellow. However, forcing the hardware beyond its design limits introduced permanent visual compromises. To keep the proportions of Q*bert's center cube consistent with the outer cubes under playfield mirroring, developers were forced to execute a tightly timed mid-line write to PF2. This workaround prevented the center cube from appearing oversized, but it generated an unavoidable, prominent vertical zig-zag gap down the center of the display, along with inconsistent lighting angles across the mirrored cubes. Retro Game Mechanics Explained shows that the Atari 2600 generated its graphics not by reading from stored frames, but by continually improvising video output within microsecond-level timing gaps of the television's electron beam. This architecture transformed physical display constraints into visible software artifacts and made strict cycle counting the defining discipline of early video game development.

精炼阅读摘要

Retro Game Mechanics Explained 指出,雅达利 2600 因没有专用视频显存,迫使 CPU 必须与显像管电子束实时同步以逐行生成画面,这种极其严苛的硬件架构造就了被称为“追赶电子束”的独特底层编程范式。 极简图形硬件与扫描线内存约束 (0:00) 雅达利 2600 的内部硬件极度简陋,主机完全没有传统意义上的帧缓冲区。在 NTSC 制式下,常规点阵显示若涵盖背景与各精灵需要约 35,857 位显存,而雅达利 2600 在任意单一瞬间仅能保存 39 位的图形数据。这 39 位被分配为 20 位的背景区域(Playfield)、两个各占 8 位的玩家精灵(Player 0 和 Player 1)、两个各占 1 位的导弹精灵以及 1 位的球体精灵。由于系统仅能存储单条扫描线的图形信息,负责游戏逻辑与输入处理的 MOS 6507 微处理器必须在电子束扫过屏幕的同时,实时逐行重写图形寄存器。 时钟周期约束与非对称背景开销 (2:10) 屏幕水平方向的 160 个像素被划分为 40 个由 4 像素组成的图块,左右半屏各占 20 个图块并共享同一个 20 位寄存器池,由 PF0、PF1 和 PF2 三个寄存器管理。硬件内置了重复与镜像模式,但若想呈现非对称背景,程序员必须在电子束扫完左半屏后、开始绘制右半屏前的狭小窗口内强行覆写寄存器。主机的 CPU 主频约为 1.19 MHz,与视觉硬件时钟锁定为 1 个机器周期对应 3 个屏幕像素。在每条扫描线总共 76 个周期的预算中,仅执行 6 次零页写入指令就需耗去 18 个周期,这还不包括从 ROM 读取数据或计算精灵坐标的开销。因此,完全定制的非对称背景极难实现,开发者大多依赖对称或重复布局以节省 CPU 时间。 缺乏坐标寄存器的精灵定位与同步机制 (12:02) 电视接口适配器(TIA)没有 X 和 Y 坐标寄存器,定位精灵必须依靠精确的时钟周期计数与频闪寄存器。向 WSYNC 寄存器写入可使 CPU 暂停并同步到下一条水平消隐(H-blank)起点。重置寄存器(如 RESP0)被写入时,精灵的原点会被锁存到电子束当前所在位置,但因内部硬件延迟会产生像素偏移。由于 1 个机器周期等同于 3 个像素,单纯靠 CPU 延迟只能实现 3 像素步进的粗调定位;若要实现逐像素精度,必须配合水平运动寄存器(HMPx)与 HMOVE 频闪进行微调(范围为向左 7 像素至向右 8 像素)。垂直方向上,硬件同样没有行计数器或同步控制,必须由程序代码通过 VSYNC 手动维持 3 条扫描线的脉冲信号,并通过 VBLANK 管理 37 行消隐期(PAL 制式为 45 行),消隐期也因此成为处理游戏逻辑的主要窗口。 多重重写与延迟寄存器的多像素图像合成 (24:26) 在《E.T. 外星人》标题画面及大型精灵渲染中,开发者展示了突破硬件极限的工程技巧。游戏中 E.T. 的面部是一个宽 48 像素、高 40 行的图形,通过将两个玩家精灵配置为每行重复 3 次且间隔 8 像素的模式,交错排列组合而成。然而,6507 CPU 仅有 A、X、Y 三个通用数据寄存器,无法在单条扫描线内同时保存绘制 6 字节数据所需的值。开发人员通过启用图形延迟寄存器(VDELP0 和 VDELP1),将 TIA 的内部锁存区转化为第四个临时存储单元,配合跨扫描线的预加载与间接寻址,在电子束行进期间连续重载并推送数据,从而在无显存支持下合成了大尺寸复合图形。 行内调色板动态切换与视觉瑕疵代价 (36:26) 不仅图形数据可以实时覆写,颜色属性也能在扫描线中途动态更改。雅达利 2600 版《Q*bert》的等轴测金字塔利用背景图形构建,同一扫描线上展现了三到四种颜色,突破了背景单颜色寄存器(COLUPF)的限制。CPU 紧跟电子束的水平扫描轨迹,在电子束移动到不同立方体切面的瞬间精确写入 COLUPF,从而在单行内完成从蓝色到黄色的切换。然而,为了修正中央立方体的比例,开发者必须在行中调整 PF2 寄存器,结合硬件镜像机制,这种极限的“追赶电子束”操作直接在屏幕中央留下了锯齿状裂隙,并导致不同立方体的受光阴影方向出现不一致的视觉瑕疵。 雅达利 2600 展现了在极端硬件匮乏下,将阴极射线管电子束的物理扫描特性直接作为系统计时基准的程序设计哲学;这种高度依赖机器周期微调与寄存器锁存间隙的开发方式,既实现了远超原始设计规格的画面表现,也伴随着因时序妥协而不可避免的视觉伪影。

Source quotes

[2:10] Retro Game Mechanics Explained: If you write the second half of the screen's data too quickly, the first half gets corrupted because the beam is still drawing that half. [15:40] Retro Game Mechanics Explained: The Atari actually has no hardware for controlling vertical syncing or blanking at all; that has to be done in the game's code as well. [36:26] Retro Game Mechanics Explained: Only one color can be used at a time, but that one color can change over time. [22:38] Retro Game Mechanics Explained: If nothing changes from one scanline to the next, no code needs to be written. [29:18] Retro Game Mechanics Explained: A write instruction needs to completely finish executing before the beam even begins drawing that part of the sprite. [18:19] Retro Game Mechanics Explained: To locate sprites horizontally down to the pixel, you need to use the horizontal movement registers.

Original transcript

[0:00] The Atari 2600 had extremely primitive hardware. [0:04] It really is a great feat of engineering and programming that any of the games made for [0:08] the system work at all. [0:10] The biggest reason why this is such a commonly stated fact is that the console has basically [0:15] no video memory. [0:17] The same processor that handled the actual game logic was also in charge of coordinating [0:22] how things appear on the screen. [0:24] And all of this had to be perfectly timed and in sync with the television's scanning [0:28] electron beam. [0:30] Hence, this kind of programming to produce an image was often referred to as 'racing [0:35] the beam'. [0:36] Here's a question: how many bits are used to produce one frame of gameplay on a standard [0:42] Atari 2600 game? [0:43] I'll give you just a few hints: the picture resolution was 160 by 192 pixels (for NTSC [0:53] systems at least), with up to 30 lines in overscan. [0:56] There was one background, two player sprites, two 'missile' sprites, and one 'ball' sprite. [1:03] And all of these had a graphical bit-depth of 1 bit per pixel, which just means that [1:08] you could only choose between two colors at a time--on or off. [1:12] Pause the video now if you wanna try to do the math. [1:20] So what did you come up with? [1:22] 160 times, say, 222 bits for the background, plus maybe 8 times 20 times 2 for the player [1:31] sprites, and 8 times 2 for the missile sprites, and just a single bit for the ball sprite. [1:37] This totals 35857 bits. [1:42] If that was your guess, you were a bit too high. [1:45] In fact, you were way too high. [1:47] The correct answer is 39 bits. [1:50] Yes, just 39 bits--I can even show that many right here on the screen for you. [1:56] 20 bits for the background--or playfield as it is called in Atari's documentation--, 8 [2:02] bits for each player sprite, and 1 bit each for the 2 missiles and ball sprite. [2:08] How in the world does this work? [2:10] First let's just focus on the playfield; we'll come back to the sprites in a bit. [2:14] I mentioned the full resolution of the output from the Atari is 160 pixels across and 192 [2:21] pixels down. [2:22] Well, the playfield has a lower horizontal resolution than that. [2:28] It can only have 40 distinct 'pixels' per scanline--let's call these blocks. [2:34] This means that each block of the playfield is actually a small group of 4 pixels next [2:39] to each other horizontally. [2:41] 40 playfield blocks times 4 pixels per block gives us the 160 total pixels across. [2:49] If we cut the number of blocks per line in half, we get 20. [2:52] This is where the 20 bits for the playfield comes from. [2:55] The playfield is actually split in half horizontally, and the left half and right half share the [3:01] same memory space. [3:02] The two halves can be repeated or mirrored; a lot of games used this mirroring functionality. [3:10] But what do you do if you don't want the playfield to be a repeated or mirrored pattern? [3:15] The general idea was that, in the game's code, the playfield bits to display the left half [3:20] of the screen were written to memory right when the scanning beam of the television was [3:25] just about to display this part of the screen. [3:28] Then, after an incredibly short time passes, the beam moves to the right drawing that part [3:33] of the screen. [3:34] During this moment, the data for the right half of the screen had to be written. [3:39] This way, two different patterns for the playfield could be drawn using the same exact memory [3:43] region. [3:45] This is where the phrase 'racing the beam' comes from, because you have to time everything [3:49] with where the scanning beam is, all the time. [3:53] If you write the second half of the screen's data too quickly, the first half gets corrupted [3:58] because the beam is still drawing that half. [4:01] If you write it too slowly, the second half gets corrupted because the beam is already [4:05] trying to draw that half. [4:06] It's a very delicate process that requires timing out exactly how long each bit of code [4:12] takes to execute, so that the writes to memory occur exactly when they need to. [4:17] Now, this process I just described was just for a single scanline. [4:22] Having only 20 bits of memory for the entire playfield, not only do the left and right [4:28] half of the screen share the same memory, but all scanlines share the same memory as [4:32] well. [4:34] This means this process of writing the data for the left and right halves of the playfield [4:38] has to occur every single scanline if you want the graphics to change as the beam moves [4:43] down the screen. [4:45] Now that we've got a general idea of what is going on, let's look at the actual memory [4:51] values used to hold the playfield data. [4:54] 20 bits can be organized into 3 bytes, with 4 bits to spare. [4:59] These three bytes are known as PF0, PF1, and PF2. [5:05] Only the higher 4 bits of PF0 are used, the lower 4 go unused, while PF1 and PF2 are used [5:11] entirely. [5:13] To draw a single scanline of the playfield, the bits are used in this order: [5:18] First, the 4 bits of PF0 are used going from least significant to most significant; so [5:24] they appear backwards. [5:26] Then, the 8 bits of PF1 are used going from most significant to least significant; they [5:32] appear forwards. [5:33] Then, the 8 bits of PF2 are used going from least significant to most significant; appearing [5:39] backwards again. [5:41] If the playfield is currently set up to display repeated, then these 20 bits are just drawn [5:46] again in the same order. [5:48] Otherwise, if it's in mirror mode, the 20 bits are drawn in reverse order to fill out [5:53] the scanline. [5:56] So in order to draw a standard line of the playfield, first, the PF0 byte should be set [6:02] before the beam even starts drawing this line. [6:05] The PF1 byte needs to be written before this point, where its bits are used to draw the [6:10] next 8 blocks. [6:11] And then the PF2 byte needs to be written before this point, where its bits are used [6:17] to draw the next 8 blocks. [6:20] The last 20 blocks are drawn using the same values--once the beam draws out all 40 blocks [6:25] of the playfield, the first 4 blocks of the next line can be set up via PF0. [6:31] Now, what if we wanted the right side to be completely different from the left side? [6:35] Not a repeated pattern, or a mirrored pattern? [6:39] Then we would just have to write to each of the playfield registers twice per line, once [6:44] for the left half, like we just did, but then a second time for the right half. [6:50] There is one thing we have to watch out for though--the playfield registers should not [6:54] be modified while their respective blocks are being drawn to the screen. [6:58] This results in garbage being written to the screen as the register is being written to [7:02] and read from at the same time. [7:05] We update the left half of the scanline like before, but then we can update the right half [7:09] independently. [7:10] The PF0 register needs to be updated sometime after this point, but before this point. [7:17] Then the PF1 register needs to be updated when the beam is somewhere between here and [7:22] here, and finally PF2 is updated after this point and before this point. [7:30] Before moving on, let's get a sense of scale for how quickly things are happening here. [7:35] How fast is the electron beam in the television moving compared to how quickly the Atari's [7:40] CPU can execute instructions? [7:43] Is updating the playfield registers just barely feasible, or is the CPU so fast that this [7:48] is no problem at all? [7:51] Recall that each playfield block is made up of 4 pixels, so let's show those with some [7:56] grid lines. [7:57] And, we're gonna zoom in a bit. [8:00] The Atari 2600's CPU, the 6507, clocks in at about 1.19 MHz. [8:07] Due to how all of the circuitry in the console is based off of the same clock signal, it [8:13] turns out that one CPU machine cycle takes the same amount of as it takes the beam to [8:18] draw out 3 pixels horizontally. [8:22] Each line, including the horizontal blanking period, takes the same amount of time to scan [8:27] as 76 machine cycles. [8:31] CPU instructions all take a different number of machine cycles depending on how complicated [8:36] they are. [8:37] For example, a simple read or write from the zero page of memory takes only 3 machine cycles. [8:44] So writing a byte to the playfield registers, which reside within the zero page, takes 3 [8:50] cycles each. [8:51] 3 machine cycles times 3 pixels is 9 pixels in total, which is 2 and a quarter playfield [8:59] blocks. [9:00] We can mark out the exact locations where the scanning beam will be at the latest possible [9:05] moment that each playfield register can be written by finding the boundaries between [9:09] the playfield blocks that correspond to the different registers. [9:14] If each of the playfield registers needed to be updated both times in one line, we see [9:19] that the first write to PF0 can be no later than 19 machine cycles after the start of [9:25] the horizontal blanking period, or H-blank, for this line. [9:30] That write will take 3 cycles, then we have a gap of 3 cycles before the next write to [9:36] PF1 needs to be made. [9:39] Continuing, we have gaps of 7, 8, 2, and 8 cycles; after the second write to PF2, there [9:46] are 11 remaining cycles before H-blank begins again. [9:50] As you can see, using 18 of the 76 available cycles for the line uses up quite a bit of [9:56] time. [9:57] And these are only the writes--the data actually being written needs to be read from somewhere, [10:02] which will take up even more of these cycles. [10:04] Additionally, this is only to display the playfield--we still have the player, ball, [10:09] and missile sprites to display too! [10:11] As well as actually executing the code responsible for handling the game's logic and everything. [10:16] It turns out that having a full, non-mirrored and non-repeating playfield design wasn't [10:22] very common, and why most games mostly stuck to some sort of background that could be mirrored, [10:27] or nearly mirrored. [10:31] Let's take a look at the sprites now. [10:33] There are two player sprites, two missile sprites, and one ball sprite. [10:39] The two player sprites use 8 bits each for their graphics. [10:43] Just like with the playfield, only one line of graphics is stored at a time, so if your [10:47] sprite changes shape vertically, it will need to be updated on the fly. [10:52] In fact, the only way to disable a player sprite is to just set its graphics to all [10:57] zeros, so you will need to update it on the fly anyway, unless your player sprite is going [11:02] to be a big vertical column spanning the entire screen. [11:05] The two missile sprites and the ball sprite is just a single bit--either on or off. [11:12] There are a pair of registers that allow the player and missile sprites to be stretched [11:17] or duplicated on the scanline, but still only 8 bits per player and 1 bit per missile sprite [11:24] exist. [11:25] However, these bits can be updated in between the time that the copies are drawn, allowing [11:30] for what looks like two distinct sprites on the same line using only one sprite's data. [11:38] Say you have a small graphic, say 8x8 pixels, that you want to display at a specific spot [11:44] on the screen, using the player zero sprite. [11:48] Let's say, 40 lines from the top of the screen, and 60 pixels from the left side of the screen. [11:54] Just write the bytes 60 to the X position register and 40 to the Y position register, [11:59] right? [12:00] If only it were that easy. [12:02] Neither of these registers exist--positioning sprites has to be done manually by counting [12:07] scanlines and machine cycles. [12:11] Let's work on the horizontal component first. [12:14] The easiest way of setting the horizontal position of a sprite is by using the reset [12:19] sprite registers. [12:21] There are five of them, one for each sprite. [12:25] When this register is written to (the value written doesn't matter since this is what's [12:29] called a strobe register), the position of the sprite will be set to the current location [12:35] of the scanning beam. [12:37] There is also a bit of delay, so it will end up being a bit to the right of where the beam [12:41] was--for player sprites that aren't stretched, that offset is 5 pixels from where the store [12:46] instruction ended. [12:48] So, in order to get our sprite to show up 60 pixels from the left edge of the screen, [12:54] we need the CPU to finish writing to this register exactly when the beam is scanning [12:59] the 55th pixel of a scanline. [13:02] How do we know where the scanning beam is at any point in time? [13:06] There is no register to read the current position of the beam. [13:11] The way you do it is by syncing up the CPU with the beam at a known position, and then [13:15] keeping track of how far along you are on your own, by counting how many machine cycles [13:20] each instruction of your code uses. [13:22] There is a special register called WSYNC which pauses the CPU until the scanning beam finishes [13:29] a scanline. [13:31] This way, we know that the instruction that runs after writing to WSYNC will execute at [13:36] the very start of H-blank, which can be thought of as being 68 'pixels' before the first visible [13:43] pixel of the line. [13:45] So after we sync up at the start of H-blank, we have to wait these 68 'pixels' along with [13:50] the 55 pixels of offset we want, before writing to RESP0. [13:56] 68 plus 55 is 123 pixels, at 3 pixels per machine cycle, we get 41 machine cycles; the [14:09] write instruction itself takes 3 cycles, so we need to wait for 38 cycles. [14:14] We need to load in the graphics for the sprite, and that will take 5 cycles if we load a constant [14:20] with an LDA instruction, and then an STA to the graphics register. [14:25] This leaves 33 cycles left to wait around. [14:29] If there's nothing else to do on this scanline, we can just use a bunch of NOP instructions [14:34] which do nothing but take 2 cycles to execute each. [14:39] There's no instruction that takes only 1 cycle, so we need a 3 cycle instruction to get to [14:46] exactly 33 cycles--that STA instruction takes 3 so may as well just execute that twice. [14:53] So for this first line of the sprite, the code will look like this: the LDA to prepare [14:58] the graphics data, the STA to store that data, another STA that does nothing but use up 3 [15:05] cycles, 15 NOP instructions, then an STA to the RESP0 register, then an STA to the WSYNC [15:13] register to wait for the next scanline to begin. [15:17] The horizontal position of every sprite is persistent until another write to the reset [15:22] position registers. [15:24] So to draw the next 7 lines of our sprite, we only have to update the graphics register. [15:30] This makes the code much shorter since we don't have to wait for anything; just load [15:34] graphics, store them, and write to WSYNC. [15:40] That will position the sprite in the correct horizontal position, but we still have the [15:44] vertical position to deal with. [15:47] Counting 40 lines from the top of the screen should be pretty easy you think? [15:51] Well, unlike with horizontal position and using WSYNC, there is no way to sync up the [15:57] CPU with the beam vertically. [15:59] The Atari actually has no hardware for controlling vertical syncing or blanking at all; that [16:05] has to be done in the game's code as well. [16:09] Vertical syncing is what the television uses to be able to tell where the start and end [16:13] of each frame is. [16:15] The vertical sync period is a small moment where the video signal becomes 'dark than [16:20] black', that is, a negative value. [16:23] Vertical syncing can be enabled and disabled with the special VSYNC register. [16:29] In order for the picture to come through clearly, VSYNC needs to be set for exactly 3 scanlines, [16:35] then reset back to zero. [16:36] This can easily be done by using the WSYNC register to make sure these reads and write [16:42] occur directly at the start of H-blank. [16:46] After resetting VSYNC back to zero, the vertical blanking period begins. [16:51] This also has a register associated with it, fittingly called VBLANK. [16:55] This should be set to 1 at the start of the vertical blanking period, and reset to zero [17:00] at the end. [17:02] Just like how the horizontal blanking period occurs when the scanning beam has to travel [17:07] from the end of one line to the start of the next, the vertical blanking period is when [17:12] the beam has to travel from the bottom of the screen to the top, starting a new frame. [17:18] There are 37 lines in V-blank in the NSTC format, or 45 lines in the PAL format. [17:24] So, in order for our sprite to show up on the 40th line, we have to wait 40 plus 37 [17:31] equals 77 lines after the end of V-sync. [17:35] We can use this extra time to actually process game logic if we had any. [17:40] The V-blank period is the only time where we don't have to worry about updating graphics [17:45] for anything, so this is a great time for handling stuff like that. [17:49] In this example though, we have nothing else to do other than showing this sprite, so we [17:54] can just write to WSYNC a bunch of times to wait out all this time. [17:58] Make sure to clear the VBLANK register at the appropriate time, then write to WSYNC [18:03] 40 more times, which is equals to the number of lines we want our sprite to show up below [18:08] the top of the screen. [18:10] After that, then we can append all of the code we wrote for setting up the graphics [18:14] and horizontal positioning of the sprite. [18:19] Let's look at a few examples. [18:21] We'll look at the title screen of E.T. since it has two main elements that show a simple [18:26] and more complex way of doing things. [18:30] First is the title text up here that says E.T. [18:32] The E is drawn with player sprite 0, the T with player sprite 1; and the two periods [18:39] are drawn with missile sprites 0 and 1. [18:43] Here is the code that positions these sprites on the screen. [18:47] This loop runs 5 times, once for each of the sprites (even the ball sprite, which is just [18:52] disabled here). [18:55] There is a write to WSYNC right at the start of the loop, so only one iteration of the [18:59] loop will run per scanline. [19:02] Inside the main loop, there are two main sections. [19:06] First, some value is read from the game's ROM and written to this register, which I'll [19:11] get to in a moment. [19:13] Then here, the lower nybble of this byte is used as a counter for how many times to run [19:19] through this small inner loop. [19:21] The purpose of this loop is simply to waste time waiting for the scanning beam to reach [19:26] a particular point. [19:29] Then the write to the corresponding reset position register occurs, before the X index [19:34] is decremented and the main loop starts again. [19:38] After the loop concludes, another WSYNC occurs, and then a write to another register we haven't [19:42] seen yet. [19:44] These two new registers are the horizontal movement and movement application registers. [19:50] This is just another way to move sprites around. [19:54] Remember how one CPU machine cycle takes just as long as it takes for the beam to scan across [20:00] three pixels? [20:02] This means if you use solely the reset position register, your horizontal resolution can only [20:08] be this good. [20:10] To locate sprites horizontally down to the pixel, you need to use the horizontal movement [20:16] registers. [20:17] By writing to the upper 4 bits of an HM register, you are queuing up a value that corresponds [20:24] to a number of pixels to move the sprite, going from 7 pixels to the left to 8 pixels [20:30] to the right. [20:32] The movement doesn't happen when this value is written, but instead the movement values [20:36] queued up for all 5 sprites are applied when the HMOVE register is written to. [20:43] In this routine, you can think of the write to the reset position registers as setting [20:48] the 'coarse' horizontal position, the write to the horizontal movement register as queuing [20:53] the 'fine' horizontal position, and the write to the movement application register as applying [20:59] the 'fine' position. [21:01] For example, the value retrieved from ROM for the big E is $52. [21:07] This byte is written to the HMP0 register, which will correspond to a movement of 5 pixels [21:13] to the left, since it only looks at the upper nybble. [21:16] Then, the lower nybble is used as the inner loop counter--this branch will be taken twice. [21:23] If we count out the machine cycles for this routine, we see that each inner loop execution [21:29] adds 5 extra machine cycles, which means the coarse horizontal position moves in steps [21:35] of 5 times 3 equals 15 pixels. [21:39] This range is covered by the fine horizontal position, since that ranges from 7 left to [21:44] 8 right, a range of 16 pixels. [21:49] For this big E sprite, the write to RESP0 finishes 36 machine cycles after the start [21:55] of H-blank. [21:57] This corresponds to 36 times 3 or 108 pixels. [22:02] Subtract the 68 pixels from the H-blank period, but add in a delay offset of 6 pixels this [22:08] time (because the sprite is stretched), and you get 46 pixels from the left edge of the [22:14] screen. [22:15] Then, after the loop concludes and the other sprites' coarse positions and HM registers [22:20] are set up, the HMOVE register is written to. [22:24] This shifts all the sprites over by a few pixels corresponding to the value stored in [22:28] the HM registers. [22:31] The big E moved to the left 5 pixels, and is now 41 pixels from the left side of the [22:37] screen. [22:38] Okay, that was the code to set up the position of the 4 sprites, but now it's time to look [22:44] at the code that updates the graphics each scanline. [22:48] Here is the code that does that. [22:51] This first block is just run on the scanline before the first line the sprite should appear. [22:56] It does stuff like sets the color of the sprites, makes sure their graphics are not flipped [23:02] horizontally, and sets the number and sizes of the sprites. [23:06] In this case, the player sprites are shown stretched at 4 pixels per bit, and the missile [23:12] sprites are 2 pixels wide. [23:14] The letters E and T are 16 units tall, so this loop is prepared to run 16 times. [23:23] All this loop does is grab a couple bytes from ROM and write them to the corresponding [23:28] graphics registers. [23:30] The graphics for the two missile sprites is only run for the last 2 iterations of the [23:35] loop, since they only appear at the bottom of the text. [23:39] You'll notice that there are two writes to WSYNC inside the loop, and that is because [23:44] each unit of these letters is actually two scanlines tall. [23:48] So what looks like one large pixel here is actually 4 pixels wide and 2 scanlines tall. [23:56] Recall that if nothing changes from one scanline to the next, no code needs to be written. [24:01] Here, only a single DEX instruction is run for the entire scanline. [24:09] Once this loop finishes all of its iterations, code for the other parts of the game starts [24:14] to run, including drawing E.T.'s face here. [24:18] This sprite is 48 pixels wide and 40 pixels tall. [24:22] Only the two player sprites are used to create it. [24:26] This is an example of a sprite that is drawn by using the setting that copies the sprite [24:30] and displays it three times in a single scanline, with 8 pixel gaps in between them. [24:36] Then, the two player sprites are horizontally positioned 8 pixels offset, so that the entire [24:42] 48 pixel range can be drawn. [24:45] The sprite is split into 6 times 40 bytes, each 8x1 strip is encoded as a single byte. [24:52] This is all stored in a table somewhere in the ROM. [24:56] The code responsible for this large sprite starts 3 scanlines in advance, since a few [25:01] things need to be set up before actually drawing it. [25:05] In the first line, the sizing and positioning of the two sprites are set up. [25:10] This write to HMOVE is not important here, a constant #$03 is loaded into the accumulator, [25:16] and #$00 is loaded into the Y index register. [25:20] The reflection registers are zeroed out here and here, to make sure the sprite is drawn [25:25] not flipped. [25:27] The sizing registers are written to, which display the sprites 3 times per line and at [25:33] 8 pixel intervals. [25:35] Then this pair of registers is written to, which we haven't seen yet. [25:39] These are the graphics delay registers. [25:42] When these registers are cleared, a write to the graphics registers goes directly to [25:47] those registers with no delay. [25:50] When VDELP0 is set, the graphics for player sprite 0 won't fully go through until the [25:57] graphics for player sprite 1 is written. [26:00] This way the graphics can essentially be applied at the same exact time. [26:04] Likewise when VDELP1 is set, the graphics for player sprite 1 won't fully go through [26:10] until the graphics for player sprite 0 is written. [26:13] Here, both registers are set, which means both registers' writes are delayed until the [26:20] other register is written to. [26:21] This is used for timing reasons--it allows an extra position value to be queued up which [26:27] lets this method of writing 6 bytes worth of graphics data quickly to work at all. [26:33] I'll explain a bit more about this when we get to that point. [26:36] Anyway, next, the graphics are zeroed out in case they weren't already, making sure [26:41] to write to each register twice because the delay settings for both registers is enabled. [26:47] Then, after a single NOP instruction to wait an extra 2 cycles, the two writes to the reset [26:53] sprite position registers occur. [26:56] If we count the machine cycles here, that works out to 39 and 42 cycles. [27:02] Multiply by 3, subtract 68 for H-blank, add 5 for beam delay, and we get horizontal positions [27:09] of 54 and 63 for the two sprites. [27:13] Oops, that's a difference of 9, not 8. [27:17] That's because this kind of STA instruction, which writes the data to the register we need, [27:23] takes 3 machine cycles, which translates to an offset of 9 pixels. [27:28] It's the closest we can get two sprites together in a single scanline of work. [27:33] No worries, we'll just use the HM register to scoot the first player sprite to the right [27:38] by one pixel to put it at horizontal position 55, and we're all good. [27:45] The next scanline, all the pointers to the sprite graphics data in ROM are set up in [27:50] RAM. [27:51] The first instruction is the write to HMOVE, since writes to HMOVE should always come right [27:57] after a write to the WSYNC register. [28:00] The rest of this just sets up the six pointers to the six columns of data for the sprite. [28:07] This is done like this because the routine that draws the sprite is actually a general [28:11] routine that can be run with any graphic. [28:14] So this is a prerequisite to calling that routine. [28:18] The third line just sets up the colors of the sprites, and initializes the loop counter [28:25] to #$27, or 39, since the loop needs to run 40 times. [28:30] Here is the JSR subroutine call that calls the general drawing routine. [28:36] Before the last setup scanline concludes, the graphics data for the left-most column [28:41] of 8 pixels is queued up by writing to GRP0. [28:44] Since the graphics data delay registers are enabled, this doesn't actually do anything [28:49] yet, but once GRP1 is written to, this write will take effect. [28:54] I'm going to refer to the 6 bytes of graphics for the six columns as A, B, C, D, E, and [29:01] F. So after this scanline, byte A is waiting [29:06] in the temporary register associated with GRP0. [29:09] Finally WSYNC is written to, and we wait for the first scanline where the sprite will actually [29:15] show up. [29:18] Here is the main loop, which is responsible for writing the graphics data at the appropriate [29:23] time. [29:24] The general method here is to write the required byte right before it is needed. [29:30] The data written to the graphics register occurs on the last machine cycle of the write [29:34] instruction. [29:35] So a write instruction needs to completely finish executing before the beam even begins [29:41] drawing that part of the sprite. [29:44] However, that means the write instruction can begin before the beam is done scanning [29:49] the previous copy of that player sprite. [29:52] This is good, because a write instruction takes a minimum of 3 machine cycles, which [29:57] corresponds to 9 pixels, which won't actually fit in between to copies of a single player [30:03] sprite. [30:05] Before looking exactly how the code does it, let's try a naive approach, adjust it until [30:11] it actually works, then compare the two. [30:15] Let's suppose the graphics delay registers are not set for now--we'll see why we need [30:20] them enabled soon. [30:23] In order to have byte F drawn to the screen in this exact position, the third write to [30:29] GRP1 needs to finish at the latest 1 pixel before the beam reaches the start of this [30:35] copy of the sprite. [30:37] Why one pixel? [30:39] Because one machine cycle translates to 3 pixels, we can't have an instruction start [30:45] or end on any pixel, just at a resolution of every 3 pixels. [30:51] And it turns out this 3 pixel machine cycle grid lines up right here. [30:56] So if this write instruction must end by this point, and it takes 3 machine cycles, or 9 [31:03] pixels, it must start here. [31:06] This means the third write to GRP0 to write byte E can only execute as late as here. [31:15] The second write to GRP1 for byte D here, and the second write to GRP0 for byte C here. [31:23] We are cutting it close here, but remember that the data actually gets written during [31:27] the last cycle, so the fact that these two cycles writing to GRP0 overlap with the first [31:34] copy of the player 0 sprite is completely fine. [31:39] Then we can tack on the two initial writes to the graphics registers here and here. [31:45] Just to make it a bit more clear, let's draw out which machine cycles actually correspond [31:50] to sprite data being written. [31:53] Now, this would work, but there is one problem. [31:58] We can't have six write instructions in a row, especially if all six data values are [32:03] different. [32:05] The 6507 CPU only has three internal registers for general-use data. [32:10] So it can only hold three values at a time. [32:14] We'll need to stick some read instructions or data movement instructions in here to actually [32:20] move six bytes around this quickly. [32:23] The first easy step we can do is push these first two writes to the graphics registers [32:28] way out here. [32:30] There's nothing stopping these from being super early, since the sprite doesn't exist [32:34] over there; there's nothing wrong with writing to the registers so early for the first two [32:40] bytes. [32:41] Since there's room, we can even just insert two read instructions before each of these. [32:47] This leaves 4 bytes to move around quickly. [32:51] We can preload three of these bytes into the A, X, and Y registers before the sprite is [32:56] drawn as well. [32:58] This way we can write three bytes in sequence easily. [33:02] The problem occurs with this fourth byte. [33:05] We can't squeeze in a fourth read instruction while the sprite is being drawn. [33:10] Even if we use a data movement instruction which is only 2 machine cycles and try to [33:17] be fancy by utilizing the stack pointer register S to temporarily hold a fourth value, it just [33:22] won't work. [33:24] The issue is right here, since this instruction to write byte C can't come any earlier or [33:30] else it will conflict with byte A being drawn to the screen. [33:35] This is why we need to set the graphics data delay registers. [33:40] Doing this will give us an extra buffer for free, since the data is held in that temporary [33:45] internal register waiting to be written, which will act as our fourth register. [33:50] So writing byte A to GRP0 doesn't actually affect anything at first, then writing byte [33:56] B to GRP1 applies byte A to the sprite graphics, then writing byte C to GRP0 applies byte B, [34:05] and so on. [34:07] Now, we can actually shift the second write to GRP0 to the very front as well, allowing [34:12] us to use the accumulator to store byte F. So after the third write, we queue up the [34:19] three CPU registers, A, X, and Y, with the three remaining values. [34:24] We strategically write to each graphics register when the beam is scanning over the opposite [34:29] sprite. [34:31] After the sixth write, all of the data is written, but that last byte is still stuck [34:36] in the temporary internal register. [34:39] In order to get byte F actually applied, we just have to execute a dummy write to the [34:44] opposite graphics register, GRP0 in this case. [34:47] This will write that last value just in time for the beam to finish drawing the rest of [34:53] the sprite. [34:56] If we look at the game's code, this is essentially what is happening. [35:01] The main difference is that reading from ROM indirectly via a pointer like this takes 5 [35:06] cycles each instead of 2 like in our mock-up, which makes things a bit more difficult. [35:12] The first write to GRP0 occurs at the end of the previous line like we saw. [35:18] The first write to GRP1 and second write to GRP0 occurs at the start of the scanline. [35:25] At this point, the two graphics registers hold bytes A and B, while byte C is ready [35:31] to go in the temporary register. [35:34] Then, bytes D, E, and F are read from ROM and prepared. [35:40] The Y index register is actually used to fetch the graphics data from ROM, so we have to [35:45] clobber that last, and use a temporary memory byte to hold the value for the accumulator. [35:50] Then, surprisingly without any waiting required, bytes D, E, and F are written to their respective [35:57] graphics data registers, and the final dummy write to GRP0 happens after that to actually [36:03] put byte F in the graphics register. [36:06] The loop counter is decremented and we jump back up and prepare for the next line. [36:12] Finally, after the entire sprite has been drawn, the loop falls out, and all of the [36:18] graphics data, sprite size, and graphics delay registers are all zeroed out. [36:26] One last example, going back to the playfield. [36:30] The cubes in the stage layout of Q-bert are made via the playfield. [36:34] You'll see it's not quite symmetric due to these center cubes in the rows with an odd [36:39] number. [36:40] Also, the playfield seems to be using 3 or 4 colors at once, instead of just one like [36:45] I said before. [36:47] Let's look through one scanline and see how this is accomplished. [36:52] Just like how the graphics data for things can be changed mid-scanline, so can the colors. [36:58] Only one color can be used at a time, but that one color can change over time. [37:03] So the playfield can be blue on a black background or yellow on a black background, but it can't [37:09] be yellow and blue at the same time. [37:13] The COLUPF register is responsible for controlling this color. [37:17] In this scanline, there are 3 cubes; the color values for the cubes are already stored in [37:23] memory, so they are just moved from there to the proper register at the correct time. [37:28] Because of the odd size of the cubes, the playfield registers, namely PF2, need to be [37:34] updated mid scanline. [37:37] This is just so that the center cube doesn't show up larger than the others--however it [37:42] does result in this large zig-zag shaped gap. [37:45] The register is written to just perfectly so that the two halves of the cube show up [37:49] properly even though they share the same bits of data. [37:53] Along with the weird gap down the middle of the screen, the mirrored playfield is also [37:57] the reason why the shading on the cubes is inconsistent. [38:01] Thank you for watching. [38:05] Retro Game Mechanics Explained has a Patreon and SubscribeStar page if you would like to [38:09] be generous and help support the channel. [38:12] Otherwise, watching the video, leaving a comment, and sharing it with your friends is a great [38:16] way to help too. [38:18] If you would like to see more about the Atari 2600, please leave a comment saying so!