Classroom / Apple II Plus

Apple II Plus: BASIC, graphics, and DOS

Working machine and executable guideView source ↗

Follow the guide here, or open the laboratory ↗ to explore the screen, processor, memory, and devices together. Each view starts its own machine.

A screen of your own

Meet Applesoft.

Choose your Apple II Plus ROM, select Run, and wait for ]. Try PRINT 2+3, then Enter. See the controls and a short program. Then draw in sixteen colours or try high-resolution graphics.

The file stays on your computer. A verified selection prepares a fresh, paused machine.

No ROM loaded.

Checking for a remembered ROM…

Boot DOS 3.3 from disk

Use the 20 KiB ROM container above, then choose the matching DOS 3.3 System Master. A verified disk selection starts a fresh, paused machine. Run boots DOS; the original file stays unchanged.

No disk loaded.

No ROM loaded · 0 instructions

APPLE II PLUS64K · APPLESOFT

Text · page 1

Letters become uppercase. Backspace or Left Arrow backs up one character. Paste one line at a time; the screen shows the computer’s response, with no local echo.

0 keyboard characters queued

Explore the ROM

Follow the reset-to-prompt walkthrough, or stop at an address of your own.

Choose a ROM to begin.


        

Highlighted values changed in the last instruction. Hover for the previous value.


        

Latest twelve completed instructions, newest first · fetched bytes, assembly, changes, then ordered memory accesses (including fetches). Opening this view does not read guest devices. Step executes the instruction at the current PC.


      

Pause preserves your program. Reset CPU preserves RAM and devices; Fresh power-on discards the program. Your ROM is remembered; RAM, programs, and disk selections are not saved when you leave or reload.

ROM walkthrough

For repeatable experiments, use the laboratory's Saved states panel at any paused boundary. Save before a routine, step through it, and Compare to see net changes; Restore lets you try the same starting point again.

The laboratory's Move the cursor by editing RAM walkthrough changes CH at $0024, follows the ROM's indirect screen store, and repeats the experiment after restoring the original cursor position. EDIT in Memory, Zero page, or Stack enables paused byte editing; turn it off to resume click-to-watch behavior. The laboratory’s Move the cursor by editing RAM walkthrough uses these tools to change CH at $0024, follow the ROM’s indirect screen store, and repeat the experiment after restoring the original cursor position. EDIT in Memory, Zero page, or Stack enables paused byte editing; turn it off to resume the usual click-to-watch behavior.

The Apple II can reach its Applesoft prompt without a disk. Follow that path in the browser machine, then watch one keypress travel from the keyboard latch to the screen. In the classroom, open Explore the ROM under the keyboard and keep it open while stepping. In the laboratory, load firmware in ROM, use Disassembly for run-to controls, and keep Execution history visible while stepping with the Execution panel. Both show the latest twelve executed instructions; select a laboratory history entry to inspect that step's changes and memory accesses. Instructions appear newest first; each instruction's accesses retain execution order. All numbers in that view are hexadecimal; flags are 0 or 1.

Choose your verified ROM. If a disk is selected, eject it, then use Fresh power-on. This walkthrough starts with fresh RAM and no disk controller ROM. Reset CPU preserves RAM and devices, so it can take a different firmware path.

Start at reset

The current PC is $FA62, the ROM's RESET entry. The CPU has obtained that address from its reset vector; no ordinary instruction has run yet. Step executes CLD, which clears decimal mode. Read the first trace entry: it shows the byte actually fetched and the next PC. An unchanged flag does not appear in the change list.

Choose HOME in the routine selector and Run to routine. The machine executes normally until it is about to enter that routine. It pauses before executing the instruction at $FC58. Step to begin following its work, or use the next stop to pass over the clearing loop.

From memory to characters

Choose COUT and run to it. This is an output entry, used for control characters as well as printable ones. Inspect A, then Step: JMP ($0036) reads the two-byte output hook. The trace records both bytes read and the destination PC. The program can change this hook; it is not a fixed call baked into the CPU.

Choose KEYIN2 and run to it. With this fresh ROM-only machine, the screen now contains the Apple banner and ]. Execution has reached the keyboard polling instruction at $FD21. The prompt is drawn by ROM code writing screen memory; the browser only renders the resulting video state.

Follow the letter A

At KEYIN2, type A into the Apple II keyboard without pressing Enter (in the laboratory, click the screen to give it keyboard focus first). The host queues the character while execution is paused. Step offers it to the keyboard latch and executes BIT $C000. The recorded read is $C1: the seven-bit code for A, with the keyboard's ready bit set. N becomes 1. Step once more to see BPL fall through instead of returning to the polling loop. Its printed operand is the branch destination, even when it is not taken.

Choose Acknowledge key and run to it. By this boundary A holds $C1, but the keyboard strobe is still set. Step executes BIT $C010. The read appears in the trace and the inspection line now says the strobe is clear. Merely opening the explorer does not acknowledge a key.

Choose STORADV and run to it. Step loads the cursor column into Y; step again to execute the indirect store. That store reads the screen pointer, then writes $C1 into text memory. Read its destination in the trace. Finally run to KEYIN2 again: the display shows ]A, and the firmware is waiting for another character. This was line editing and echo; Applesoft has not executed a command because no Enter has arrived.

Inspect the cell that received A

In the laboratory, turn on Inspect beside the screen's display options, then select the A just echoed. The strip below the screen shows row 2, column 1, page 1, address $0501, byte $C1, and normal. Rows and columns count from zero. The generated video specification supplies the address and character interpretation; the inspector reads physical RAM, without a guest bus access. Arrow keys move the selection. Escape or turning Inspect off returns the screen to typing. While Inspect is on, keys and paste do not enter the machine.

The address opens Memory at that byte. Watch writes opens Watches and enables its W stop, preserving any existing label or other stop choices. Subsequent Run stops after an instruction writes that address, even if the byte stays the same. Disable W when continuing a walkthrough that would otherwise stop there.

Last write identifies the last completed CPU instruction observed writing this physical byte, with its instruction number, captured assembly, memory mapping, and before/after values. Immediately after the echo, $FBF2 is STA ($28),Y: STORADV loaded column CH from $24 into Y; BASL/BASH ($28/$29) held $0500; adding Y selected $0501. Clicking the writer address browses Disassembly's current bytes if the recorded bank is still visible. The captured instruction remains evidence even if RAM code has since changed.

This is the last store, not necessarily the instruction that introduced the character. Cursor animation can overwrite a cell repeatedly. Scrolling later copies characters with a different instruction. A store of the same value still becomes the last writer. Read-modify-write instructions retain their final store.

The observer keeps one writer per byte of both text pages, independently of the Changes log's recording switch, Clear button, or retention limit. Reset, fresh power-on, successful media replacement, register editing, and execution errors clear this history. A cell with no captured store says so; it does not attribute initial RAM to an instruction. If an external change differs from the captured value, the strip marks that difference. Selecting a coordinate follows the currently displayed page, while each page retains its own writers. Graphics pixels have no text-cell interpretation; mixed mode still exposes its four text rows. The observer does not retain a complete history of a cell.

Carriage return is cursor work

For this part, use Fresh power-on, then run to KEYIN2 again. Leave Inspect off to type. Press Enter on the empty prompt and run to COUT. A contains $8D, the carriage-return code with its high bit set. Run to CR ($FC62). Two steps execute LDA #$00 and STA $24, setting CH to zero. The next instruction, at LF ($FC66), increments CV at $25 from 2 to 3.

These are cursor-variable writes, not screen-character writes. The ROM compares the new row with WNDBTM at $23; below that boundary it calculates the new screen base. Follow $24, $25, and $28/$29 in Zero page or Watches. Screen rows are interleaved in RAM: the first eight begin $0400, $0480, $0500, and so on, but row 8 begins $0428. The base cannot advance by a constant forty bytes at every row boundary.

Run to KEYIN2 to reach the next prompt. Its row is now 4, because the Applesoft path back to the prompt emitted another newline after the echoed carriage return. A ROM routine's local effect and the whole command-input path are different observations.

Scrolling is a sequence of ordinary stores

Starting at that fresh empty prompt on row 4, repeat Enter, Step once to leave the current polling address, then Run to KEYIN2, nine times. The prompt reaches row 22. Press Enter once more, then run to SCROLL ($FC70). The ROM has reached the bottom of the window: WNDTOP ($22) is 0, WNDWDTH ($21) is 40 ($28 in hex), WNDBTM ($23) is 24 ($18), and CV is held at 23.

Run to address $FC8E. The preceding LDA ($28),Y has read from the next row; STA ($2A),Y will write into the preceding row. On this first iteration Y is 39, the source is $04A7 (row 1, column 39), and the destination is $0427 (row 0, column 39). Select that destination in Inspect and Step. Its last writer now names $FC8E, even if both bytes were spaces.

Run to address $FC95 to finish the copy loops. Rows 0–22 now contain the old rows 1–23; each copied cell's writer is $FC8E. Run to $FCA0 and Step to see STA ($28),Y begin clearing the bottom row with normal spaces ($A0), starting at $07D0. This store belongs to CLEOLZ, not STORADV. Finally run to KEYIN2: the prompt is on the bottom row and the machine is waiting again.

There is no browser scroll command in this path. The CPU executes the ROM's loads and stores, RAM changes, and the display renders those bytes. Screen inspection connects each visible result to that execution.

Explore beyond the walkthrough

Run to address accepts one to four hexadecimal digits, optionally preceded by $. It sets a one-shot address breakpoint, checking PC before each instruction. If already at that address, it stops immediately; Step first to leave it. Routine stops also require motherboard ROM to be mapped there. An address stop can instead target RAM, including code loaded by DOS, without assigning it a ROM label. Every run-to request pauses after at most two million instructions if its destination is not reached. It does not claim the program finished.

Pause cancels a pending run-to request. Step and ordinary Run also clear it. Reset, fresh power-on, media selection, and leaving the page cancel it too. The trace describes completed instructions, not a speculative listing of the next bytes. Its ordered memory accesses include instruction fetches as well as data reads and writes; the fetched bytes also appear beside the assembly. Recorded ROM labels retain the bank mapping at execution time. The explorer does not read through the guest bus or inspect disk bytes. Captured history uses execution records; the separate Instruction panel previews a copied CPU against safe storage observations. The machine's model limitations still apply.

Breakpoints and subroutine stepping

In the laboratory, the circle beside each Disassembly row toggles an instruction breakpoint. A filled circle is enabled; an empty circle is inactive. Breakpoints below the listing also accepts addresses directly, with individual enable and remove controls. Choose ROM only for motherboard firmware or Any mapping for an address regardless of its current bank. Row markers in mapped motherboard ROM default to ROM-only stops. Up to 64 breakpoints are remembered in this browser; reset, fresh power-on, and loading media retain them. These are address preferences, not saved machine state or proof that the same software still occupies RAM.

Breakpoints stop before the instruction and before offering queued keyboard input. After a breakpoint stops execution, Run crosses that boundary once, then stops again on the next visit. Run stops immediately at a newly added breakpoint at PC. The stepping controls cross the current boundary once; Step always executes exactly one instruction, even at an enabled breakpoint.

The Execution panel adds Step over and Step out. Step over a JSR follows execution until that particular call returns, including nested calls and tail jumps; on another instruction it executes one step. Step out finishes the most recent observed call (or interrupt frame). It is unavailable until a caller has been observed. Neither control changes PC or reconstructs callers from arbitrary stack bytes. Reset, fresh power-on, media replacement, register editing, and execution errors clear the observed history.

Calls are recognized from the CPU specification's PC/SP writes and ordered stack accesses. The tracker checks the returning PC and stack pointer against the observed call, handles nested BRK/RTI frames, and retains at most 128 frames. If software replaces SP or returns somewhere unexpected, a pending subroutine step stops and reports that caller tracking was lost. This matters for software that uses the stack for dispatch or edits return addresses. Hardware interrupt entry is not automatically polled by this machine.

Call stack, initially a tab beside Stack, shows the active observed calls, innermost first. Each entry names the call target, call site, continuation address, and SP before the call. BRK frames are distinguished from JSR calls. ROM labels come from this guide. Click an address to browse Disassembly without running it. Call-site and entry mappings are captured when the instruction executes; their links are disabled if a different bank is now mapped. Continuation links browse current memory: a program may overwrite the saved return address. A lost-history notice remains visible even if later calls establish a new partial stack. Reset, power-on, media replacement, or a register edit starts a new observation history. The view does not reconstruct calls that were not observed.

A watchpoint stops after the matching instruction, before the next instruction breakpoint or completed step/run-to request is checked. Instruction breakpoints then take precedence over a completed step or run-to request. A breakpoint, Pause, or leaving the page cancels the temporary request; Step out can be selected again using the retained call history. Run-to, Step over, and Step out have a two-million-instruction limit so a polling loop cannot run indefinitely as a step. The Execution and Disassembly panels distinguish breakpoint stops, completed steps, reached addresses, manual pauses, instruction limits, lost callers, and execution errors. Ordinary Run continues until paused, stopped at a breakpoint or watchpoint, or an execution error occurs.

Live disassembly

The laboratory's Disassembly panel shows a sixteen-row listing. Its PC / MEM header toggle chooses the source. PC places the last three executed instructions above the processor's next instruction, followed by a live listing of current memory. Stepping moves the view with it. MEM starts at Memory's selected address: enter a hexadecimal address in Memory's header and press Enter, or scroll its contents.

Navigate or run to an address exposes explicit navigation and execution controls. Enter an address and choose Browse to select MEM and move Memory there. Next 16 continues after the last displayed instruction. Browsing never executes or changes the processor's PC. Run to address instead executes until PC reaches that address.

The laboratory header's ← / → buttons move back and forward through explicit address browsing in Memory, Disassembly, and the ROM reference. Following a pointer or a reference records the address you left, so you can return without typing it again. PC following, memory scrolling, and selecting PC/MEM modes do not add entries. A new destination after Back replaces the forward path. The latest 64 locations last for this visit; they are addresses, not machine snapshots. Returning selects the captured address in the current storage mapping, without running instructions or restoring a bank. Disassembly returns in MEM mode, Memory in FIX, and ROM to a selected address.

The dimmed rows marked · show actual execution in chronological order, including repeated visits to an address. A solid horizontal line appears below an unconditional transfer (JMP, JSR, RTS, RTI, or BRK); a dotted line appears below a conditional branch, whether taken or not. These markers apply to both executed and upcoming instructions, even when the target happens to be the following address. The → arrow marks only the next instruction, even if its address also appears in the history. Before three instructions have run, there are fewer history rows. Address browsing turns off this history prefix.

Select an instruction's address to run to it. This uses the same bounded, one-shot address stop described above, pausing before execution in any bank. An address button is available only when all its instruction bytes are visible and firmware is installed. Use Step to execute the stopped instruction. The arrow marks PC when that address appears in the listing. Known routine names on live rows appear only while motherboard ROM is mapped at those addresses; Language Card RAM does not inherit ROM labels. Executed rows retain their recorded ROM mapping.

The Routine column, before Address, names a documented ROM entry at that instruction's exact address. The Operand column on the right labels the encoded address: a JSR, direct JMP, or branch destination, but also a load, store, comparison, BIT, or other memory operand. Control destinations keep their arrow (→ SETNORM); data references use their symbolic operand notation (INVFLG, FMT1,X, or (BASL),Y). Immediate values are not addresses. Branch labels describe the possible target whether taken or not.

Indexed labels name the encoded base, not the address after adding X or Y. Indirect labels name the pointer, not its contents or destination. The listing never reads that pointer or a device to resolve a label. Incomplete instructions do not receive operand labels. Both columns open the corresponding reference.

The guide owns selected labels alongside its routine stops. Each label declares its scope: rom data requires the identified ROM to be mapped; workspace names apply only to instructions in that ROM, since other software can reuse RAM; hardware addresses remain identifiable regardless of ROM banking. History uses the fetched bytes and mapping captured before execution. Hardware names describe addresses, not a claim that every peripheral is emulated; the machine chapter owns device behavior and limitations.

The CPU chapter supplies instruction names, lengths, and control-flow behavior. Decoding proceeds forward from the selected address, without following jumps or reading data operands. Data can look like valid instructions, and starting partway through an instruction produces a different listing. An unsupported opcode appears as .byte $XX and advances one byte. Unavailable storage appears as --; missing operands appear as question marks. Neither offers a run-to button. BRK includes its fetched padding byte. Operand bytes wrap at FFFF as on the CPU, but the listing itself stops at the end of the address space.

Rows from PC onwards show memory now. The history rows preserve the bytes actually fetched, as does the longer Execution history panel; self-modifying code can make those views differ. Both observations leave the machine untouched. The live listing reads RAM, mapped ROM, and the installed slot ROM directly, never a keyboard, disk, or Language Card switch through the guest bus. Those device addresses remain unavailable even when the machine can read them during execution.

Exploring the ROM reference

The laboratory's ROM panel connects reference material to the existing inspectors. Search by name, hexadecimal address (with or without $), or words in the description. Selecting an entry holds its description. Browse code opens Disassembly at that entry; View memory opens Memory there in Fixed mode. Neither action executes instructions or changes PC. A named label in Disassembly opens its ROM reference in the other direction. Execution remains in the Execution panel and Disassembly's explicit run-to controls.

Follow PC locates the processor's current address. Follow Memory uses Memory's selected address, including when that view is held by its Live control. ROM's own Live control governs automatic reference updates during Run. Search and selection remain available while held. Following shows the nearest documented entry at or before the address within the same ROM region, with an explicit offset; proximity does not prove that execution is inside that routine. Search also includes the guide's workspace, hardware, and ROM data labels. Selecting one shows its exact address and description; these labels do not become routine stops or inferred routine boundaries. The reference is not an exhaustive symbol table.

The reference can be browsed without loading firmware. PC and Memory following only associate addresses with ROM when the verified image is installed and mapped. When Language Card RAM covers ROM, the panel says so; selecting a reference still explains the ROM, while Disassembly and Memory show the storage that is actually mapped. The Monitor uses scratch addresses such as $28–$29 and $3A–$3B; these are software variables, distinct from CPU registers. Other software can reuse them.

Guided exploration in the laboratory

Walkthrough, initially a tab beside ROM, brings five paths into the workspace: reset and echo, carriage return, scrolling, cursor editing, and output-hook redirection. Choose a walkthrough and a checkpoint in any order. Previous and Next change only the selected explanation; they never execute, queue keys, reset hardware, or claim a step is complete. Each checkpoint describes its preparation and the observations to compare.

Run to checkpoint uses the existing ROM-only run-to request, with its instruction limit and normal breakpoint/watchpoint handling. It stops before the target instruction. If already there, it stops without executing; Step once when you want to leave a polling address before returning to it. Other stops remain visible, and selecting another checkpoint during execution does not retarget the pending run. Matching PC and ROM mapping means only that the address has been reached, not that the described setup or result has been verified. Reset and media changes retain the selected reading position, without preserving any claim about previous execution.

Browse code opens the checkpoint in Disassembly without running it. Routine reference opens its documented routine. The reference gives inputs, effects, workspace-address links, and related routines for COUT, STORADV, CR, and SCROLL. These describe this ROM's conventions, not additional CPU registers or inferred routine boundaries. They remain readable without installing firmware.

Disassembly's Notes toggle shows a separate ROM comment column beside the operands. Its comments explain selected instructions in their software context. They appear only for complete instructions in the identified ROM, with bytes matching the guide. Executed rows use the bytes and mapping captured at execution time; Language Card RAM at the same address never inherits a ROM comment. The CPU specification still owns instruction decoding and general instruction explanations.

Watching the hardware

System separates the live memory map's read and write destinations. At power-on, ROM supplies $D000–FFFF reads while Language Card RAM accepts writes. The lower window identifies bank 1 or bank 2; the upper window uses shared RAM. Protection, bank selection, and absent hardware are visible without performing guest reads. These are routing destinations: individual device bindings may ignore a transfer. Unanswered reads supply $00 in this model, not a simulated floating-bus value.

Below the map, Keyboard, Display switches, and Language Card show their retained state. Amber marks a change since the previous displayed sample; hover for the previous value. During Run this can span multiple instructions, so an unchanged field does not prove there was no intervening activity. Live controls sampling; pausing or stepping refreshes it. Each device links to its executable description.

Device activity, initially beside System, shows actual CPU transfers in $C000–CFFF, newest first. Follow the keypress above: BIT $C000 at $FD21 observes $C1, then BIT $C010 at $FD2B acknowledges it. Entries retain the instruction bytes and mapping, accessed address, direction, and transferred value. Slot-ROM fetches are distinguished from data reads. Click the instruction to browse Disassembly, or the accessed address to open Memory. Disassembly navigation refuses an instruction whose observed bank is no longer mapped; it displays current code, which may differ from the captured bytes.

The device filter selects Keyboard, Display, Language Card, Disk II, or unmapped accesses. Descriptions come from the declared bindings and link to the specification. They explain the operation; the byte beside it is the actual recorded transfer. Host key delivery has no responsible instruction and is not invented as a CPU event. Failed instructions are excluded; their partial effects remain available in Changes.

The latest 128 groups are retained even while the panel is hidden or Live is off. Successive identical device accesses from the same instruction coalesce, retaining their count and first/last instruction numbers; ordinary instructions between polls do not split a group. Different instruction bytes, mappings, addresses, directions, or values do. The status reports discarded older accesses. Clear restarts counts without restarting instruction numbering. Reset, fresh power-on, replacement media, restore, and register/RAM edits start a new history.

Experimenting with the Monitor workspace

In the laboratory, Zero page → NAMES shows the Monitor's named bytes and little-endian words. For example, BASL/BASH combines $28–$29 into the text row pointer, and CSWL/CSWH combines the output hook. The guide explicitly declares each word with bytes: 2; neighbouring names alone do not imply a word. The hexadecimal view remains available. Values reflect RAM, and other programs may use the same addresses differently. Click an address to inspect its bytes in Memory. In NAMES, FIX / CHG keeps the scroll position fixed or follows the latest changed named value, including either byte of a word. CHG remembers changes between display refreshes; Live still controls whether the running inspector refreshes. The position preference is saved.

Memory, Zero Page, and Stack also mark the next instruction's accesses: dotted underline for instruction fetches, cyan underline for data reads (including pointers), and a violet outline for writes. A byte can carry more than one mark; amber still means it changed in the previous instruction. A paired workspace word carries marks for either byte, with exact addresses in its tooltip. Stack entries also show pending accesses below SP, so a push is visible before it executes. Writes are predicted bus requests, not claims that ROM or a device will store the byte. A blocked device read ends the prediction at that address.

The TXT toggle beside 8 / 16 adds an Apple II character column to a byte view. It uses the video specification's uppercase character decoding: inverse characters have a light background, flashing characters are amber without animation, and unavailable storage is a dot. Hover for the attribute and byte. This is a text interpretation of memory, not a claim that all bytes are text. Each inspector saves its choice. Reformatting a held view retains its displayed bytes and access marks; it does not sample newer machine state.

Watches, initially a tab beside Memory, keeps a saved list of byte addresses and optional labels. Values show hex and decimal. A highlight means the value changed since the previous displayed sample, which can span many instructions during Run. Language Card values follow the currently mapped bank; a bank switch may therefore change a displayed value without a write. Hardware addresses show unavailable storage rather than reading a device.

Each watch also has independent R / W / Δ stop toggles, saved with its address and label. R matches recorded data reads, including pointer and stack reads, but excludes instruction and operand fetches. W matches bus writes, including unchanged stores and requests ignored by ROM or protected memory. Δ matches actual changed RAM bytes, including either Language Card bank and upper RAM hidden behind ROM. It observes individual writes, not differences between displayed samples; merely switching banks is not a RAM change. With all three toggles off, the watch observes without stopping execution. Earlier saved watches keep that stop behavior.

Outside Edit mode, click a hexadecimal byte in Memory, Zero page, or Stack to add it to Watches, then choose its stop toggles. Watch beside a NAMES entry uses the guide's declared width, including both bytes of a word. A regular byte click preserves an existing watch's width, label, and stop choices.

Watches has a compact BYTE / WORD choice for new and existing entries. Words combine two consecutive bytes, low byte first; both must be available. They do not wrap at $FFFF, so the last possible word starts at $FFFE. The same consecutive-byte rule crosses $00FF into $0100; this is a storage view, not the 6502's indirect-addressing wrap behavior. Changing width starts a new displayed-value comparison. Widths are saved with labels and stop choices; older saved watches remain byte-sized. At most 64 starting addresses are saved.

Click a word's displayed value in Watches or NAMES to browse it as an address in Memory. The source-address link still opens the bytes holding the word. This uses the displayed value, including a held value, without reading through the pointer or changing a bank. Not every word is a pointer: the random-number workspace can also be interpreted this way, but is still just a number.

A word's R / W / Δ stops cover either source byte and report the actual accessed address and byte value. Overlapping byte and word watches combine their enabled stops; their order in the list cannot hide a match.

Stops happen after the complete instruction, even with Watches hidden, Live off, or change-log recording paused. The first matching access in instruction order wins; when W and Δ both match a write, the stop is reported as W. Execution and Disassembly identify the responsible instruction, watched address, captured value, and physical RAM bank and before/after values when a changed write provides them. Read and unchanged/ignored-write stops do not invent earlier values or reread memory. A stop cancels temporary stepping or run-to requests while retaining observed callers. Run resumes at the next boundary, where a separate instruction breakpoint may still stop it. Manual Step can trigger another watchpoint. Failed instructions remain execution errors; partial effects stay in the change log.

History of watched addresses

Each watch's History button shows captured data reads and bus writes to that byte or word, oldest first and in instruction access order. Add the watch before running: the tool cannot reconstruct earlier use. Stop toggles do not affect capture. It continues with Watches hidden, Live off, or Changes recording paused; fetching instructions and operands does not count as a data read.

Each entry identifies the executed instruction and accessed byte. Reads show the captured bus value. Physical RAM writes include their bank and before/after values, including unchanged stores and the two stores of a read-modify-write instruction. A device or protected-ROM write shows its bus value without inventing a previous value. Word watches include either byte, and overlapping watches never duplicate an access within a watch's history.

The latest 256 accesses for each watch are retained, with an explicit count of older discarded accesses. A busy watch does not displace another watch's records. Removing a watch releases its history; changing its width cannot supply earlier accesses to the newly included byte. History selects the range to display; choose it again after changing a watch's width. Clear all watch history clears the retained entries without restarting instruction numbering. Reset, fresh power-on, state restoration, media changes, and register/RAM edits start a new history. Only completed instructions are included; failed instructions remain execution errors with partial effects in Changes.

Instruction links browse current Disassembly if the recorded bank remains visible; the captured instruction bytes remain in history even if RAM changes. Address links browse current Memory. Neither action rereads a device or changes PC. Use the header arrows to return from these address jumps.

Click A, X, Y, SP, or PC in MOS 6502 while paused to edit a hexadecimal value. Enter applies it; Escape or leaving the field cancels. An edit clears execution history and run-to stops, preserving RAM, peripherals, media, and queued input. For example, setting PC to a routine entry chooses where the next instruction runs; it does not perform a JSR or construct a return address on the stack.

The Instruction panel shows observed operand reads/writes and address calculations, including zero-page indexing and wrapped indirect pointers. Address links open Memory. Assignments remain visible even when a register or flag retains its value: CLD can show D 0 → 0. How this instruction works contains prose and symbolic calculations extracted from the executable CPU specification. The values in the preview still come from executing the generated CPU against a private copy; the explanation does not implement another ALU. Device reads stop prediction without operating a switch. Disassembly's encoded operand addresses also link to Memory; its routine and operand labels continue to open the ROM reference.

Redirecting character output

The laboratory's Redirect the ROM's character output walkthrough replaces the output hook with a tiny RAM routine. Before running from fresh ROM-only power-on, add a word watch at $0036 and a byte watch at $0310, with stops off. At the keyboard prompt, open the word's History: $FEA9 stored $F0 at $0036, then $FEAB stored $FD at $0037. The ROM has installed its original $FDF0 output handler. Browse either instruction and try Back/Forward.

Save the machine, then enter these bytes with Memory → EDIT. Inspect the initialization history first: RAM edits start a new capture.

0300: 8D 10 03 60
0036: 00 03

The first four bytes encode STA $0310 followed by RTS. The two bytes at $0036–$0037 are the output hook, low byte first: $0300. Install the handler before changing the hook, and stay paused until both bytes of the hook are set. This address choice is for the fresh ROM-only experiment; it is not a general claim that other software leaves these RAM locations unused.

Type A and stop at COUT ($FDED). Its JMP ($0036) reads the new pointer and transfers to $0300. Step the store and return. $0310 receives $C1, and RTS consumes the caller's existing return address, continuing at $FD4A. The indirect jump itself did not make another stack frame. Back in KEYIN2, the line editor has accepted the character but the handler did not advance CH or echo it to the screen. No Enter is needed.

The word's History now shows two reads by $FDED, in low/high order. The byte watch's History shows $0300 writing $00 → $C1 at $0310. These observations connect the hook's value to its use and then to the custom handler's effect.

This illustrates a software hook: ROM code chooses its destination through a RAM word. The custom routine captures the character; it does not reproduce the normal handler's control-character processing or screen effects. Restore the saved state to remove both the hook change and the RAM routine, then repeat the keypress to see the original output path. The walkthrough's automated check executes both paths with the identified ROM.

ROM identity and stops

This record supplies the browser's searchable reference, routine selector, trace, and live-code annotations. Optional routine details provide inputs, outputs, workspace names, and related routine names. Instruction notes bind prose to an exact ROM address and instruction bytes. walkthroughs own selectable checkpoints with preparation and observation text; they contain no executable setup scripts. The build validates their cross-references and instruction lengths. The region bounds describe ROM areas, not routine ends. Routine entries and software labels describe the motherboard image identified below; hardware labels describe machine addresses. The build checks the image identity against the machine's firmware declaration and validates each label's address and scope, including the bounds of declared workspace words. These selected entry points are not a full symbol table. “Acknowledge key” names an instruction within KEYIN, rather than a separate subroutine.

{
  "sha256": "378ba00c86a64cca49cedaca7de8d5d351983ebc295d9d11e0752febfc346249",
  "regions": [
    {"start": "D000", "end": "F7FF", "name": "Applesoft ROM"},
    {"start": "F800", "end": "FFFF", "name": "Monitor ROM"}
  ],
  "routines": [
    {"address": "FA62", "name": "RESET", "description": "Begin the autostart firmware's reset path."},
    {"address": "FC58", "name": "HOME", "description": "Clear the text window and move its cursor home."},
    {
      "address": "FDED",
      "name": "COUT",
      "description": "Send A through the output hook at $0036–$0037.",
      "details": {
        "inputs": "A contains the character or control code to send. CSWL/CSWH selects the output handler.",
        "outputs": "Jumps through the output hook without interpreting the character itself. Register preservation and output effects belong to the selected handler; the normal screen hook is COUT1.",
        "workspace": [
          "CSWL"
        ],
        "related": [
          "COUT1",
          "STORADV",
          "CR"
        ]
      }
    },
    {"address": "FD0C", "name": "RDKEY", "description": "Prepare the cursor and enter the input hook at $0038–$0039."},
    {"address": "FD1B", "name": "KEYIN", "description": "Wait for a key while updating the random-number seed."},
    {"address": "FD21", "name": "KEYIN2", "description": "Test the keyboard ready bit with BIT $C000."},
    {"address": "FD2B", "name": "Acknowledge key", "description": "Read $C010 to clear the keyboard strobe."},
    {
      "address": "FBF0",
      "name": "STORADV",
      "description": "Store A at the text cursor, then advance it.",
      "details": {
        "inputs": "A is the screen byte to store. CH is the column within the text window; BASL/BASH points to the current row at its left edge.",
        "outputs": "Loads CH into Y, stores A through (BASL),Y, and increments CH. Reaching WNDWDTH takes the carriage-return path; that can advance the row or scroll the window.",
        "workspace": [
          "CH",
          "BASL",
          "WNDWDTH",
          "CV"
        ],
        "related": [
          "COUT1",
          "CR",
          "SCROLL"
        ]
      }
    },
    {"address": "F800", "name": "PLOT", "description": "Draw a low-resolution pixel using row A and column Y."},
    {"address": "F847", "name": "GBASCALC", "description": "Convert a graphics row into its screen-memory base address."},
    {"address": "F864", "name": "SETCOL", "description": "Expand the colour in A into both halves of COLOR ($30)."},
    {"address": "F871", "name": "SCRN", "description": "Return the colour at row A, column Y."},
    {"address": "F882", "name": "INSDS1", "description": "Decode an instruction at the Monitor pointer $003A–$003B."},
    {"address": "FB2F", "name": "INIT", "description": "Initialize the text window and cursor."},
    {"address": "FBC1", "name": "BASCALC", "description": "Compute a text row address in BASL/BASH ($28–$29)."},
    {"address": "FC22", "name": "VTAB", "description": "Recompute the text pointer from CV and WNDLFT."},
    {
      "address": "FC70",
      "name": "SCROLL",
      "description": "Move the text window upward by one row.",
      "details": {
        "inputs": "WNDTOP, WNDBTM, WNDLFT, and WNDWDTH define the text window. The normal LF path enters with CV at the last row.",
        "outputs": "Copies each following row into the preceding row, then clears the bottom row to normal spaces ($A0). BASL/BASH and BAS2L/BAS2H serve as source and destination pointers during copying; these are ordinary CPU loads and stores.",
        "workspace": [
          "WNDTOP",
          "WNDBTM",
          "WNDLFT",
          "WNDWDTH",
          "CV",
          "BASL",
          "BAS2L"
        ],
        "related": [
          "CR",
          "LF",
          "VTAB",
          "CLREOL"
        ]
      }
    },
    {"address": "FC9C", "name": "CLREOL", "description": "Fill the remainder of the current text row with spaces."},
    {"address": "FD6A", "name": "GETLN", "description": "Print the prompt and collect an edited line at $0200."},
    {"address": "FD8E", "name": "CROUT", "description": "Send a carriage return through COUT."},
    {"address": "FDF0", "name": "COUT1", "description": "Apply INVFLG and route a character to the screen handler."},
    {"address": "FE80", "name": "SETINV", "description": "Set INVFLG ($32) to $3F for inverse characters."},
    {"address": "FE84", "name": "SETNORM", "description": "Set INVFLG ($32) to $FF for normal characters."},
    {"address": "FE89", "name": "SETKBD", "description": "Point the input hook at KEYIN."},
    {"address": "FE93", "name": "SETVID", "description": "Point the output hook at COUT1."},
    {"address": "F819", "name": "HLINE", "description": "Draw a horizontal low-resolution line, ending at H2."},
    {"address": "F828", "name": "VLINE", "description": "Draw a vertical low-resolution line, ending at V2."},
    {"address": "F832", "name": "CLRSCR", "description": "Clear the low-resolution graphics screen."},
    {"address": "F836", "name": "CLRTOP", "description": "Clear the graphics area above the mixed-mode text window."},
    {"address": "F8D0", "name": "INSTDSP", "description": "Print the decoded instruction."},
    {"address": "F941", "name": "PRNTAX", "description": "Print A and X as a hexadecimal word."},
    {"address": "F948", "name": "PRBLNK", "description": "Print three spaces."},
    {"address": "FAA6", "name": "PWRUP", "description": "Continue initialization after detecting a cold start."},
    {"address": "FB1E", "name": "PREAD", "description": "Measure a paddle timer."},
    {"address": "FB39", "name": "SETTXT", "description": "Select text display and a full-height text window."},
    {"address": "FB40", "name": "SETGR", "description": "Select mixed graphics and its bottom text window."},
    {"address": "FB4B", "name": "SETWND", "description": "Set window top from A and initialize its other bounds."},
    {"address": "FB60", "name": "APPLEII", "description": "Clear the screen and display the startup title."},
    {"address": "FC42", "name": "CLREOP", "description": "Clear from the cursor through the text window."},
    {
      "address": "FC62",
      "name": "CR",
      "description": "Return the cursor to the left edge, then advance its row.",
      "details": {
        "inputs": "The text window and cursor variables describe the current output position.",
        "outputs": "Sets CH to zero and falls through to LF, which increments CV. Below WNDBTM the ROM recalculates the row pointer; at the bottom it restores CV to the last row and enters SCROLL. No carriage-return byte is stored on screen.",
        "workspace": [
          "CH",
          "CV",
          "WNDBTM",
          "WNDLFT",
          "BASL"
        ],
        "related": [
          "LF",
          "VTAB",
          "SCROLL"
        ]
      }
    },
    {"address": "FC66", "name": "LF", "description": "Advance the cursor row, scrolling when necessary."},
    {"address": "FCA8", "name": "WAIT", "description": "Delay according to A."},
    {"address": "FDDA", "name": "PRBYTE", "description": "Print A as two hexadecimal digits."},
    {"address": "FDE3", "name": "PRHEX", "description": "Print the low nibble of A as a hexadecimal digit."},
    {"address": "FE2C", "name": "MOVE", "description": "Copy the Monitor-selected memory range."},
    {"address": "FE36", "name": "VFY", "description": "Compare the Monitor-selected memory ranges."},
    {"address": "FE5E", "name": "LIST", "description": "Disassemble instructions at the Monitor pointer."},
    {"address": "FF3A", "name": "BELL", "description": "Request the bell sound."},
    {"address": "FF65", "name": "MON", "description": "Enter the Monitor command prompt."}
  ],
  "labels": [
    {"address": "0020", "name": "WNDLFT", "description": "window left edge", "scope": "workspace"},
    {"address": "0021", "name": "WNDWDTH", "description": "window width", "scope": "workspace"},
    {"address": "0022", "name": "WNDTOP", "description": "window top", "scope": "workspace"},
    {"address": "0023", "name": "WNDBTM", "description": "window bottom", "scope": "workspace"},
    {"address": "0024", "name": "CH", "description": "cursor horizontal position", "scope": "workspace"},
    {"address": "0025", "name": "CV", "description": "cursor vertical position", "scope": "workspace"},
    {"address": "0026", "name": "GBASL", "description": "graphics base address low", "scope": "workspace", "bytes": 2},
    {"address": "0027", "name": "GBASH", "description": "graphics base address high", "scope": "workspace"},
    {"address": "0028", "name": "BASL", "description": "text base address low", "scope": "workspace", "bytes": 2},
    {"address": "0029", "name": "BASH", "description": "text base address high", "scope": "workspace"},
    {"address": "002A", "name": "BAS2L", "description": "secondary text base low", "scope": "workspace", "bytes": 2},
    {"address": "002B", "name": "BAS2H", "description": "secondary text base high", "scope": "workspace"},
    {"address": "002C", "name": "H2 / LMNEM", "description": "Line endpoint or left mnemonic byte; shared scratch storage.", "scope": "workspace"},
    {"address": "002D", "name": "V2 / RMNEM", "description": "Line endpoint or right mnemonic byte; shared scratch storage.", "scope": "workspace"},
    {"address": "002E", "name": "MASK / FORMAT / CHKSUM", "description": "Graphics mask, instruction format, or tape checksum.", "scope": "workspace"},
    {"address": "002F", "name": "LENGTH / LASTIN", "description": "Decoded instruction length or previous input byte.", "scope": "workspace"},
    {"address": "0030", "name": "COLOR", "description": "lo-res colour / HMASK (hi-res bit mask)", "scope": "workspace"},
    {"address": "0031", "name": "MODE", "description": "monitor mode", "scope": "workspace"},
    {"address": "0032", "name": "INVFLG", "description": "inverse flag ($FF=normal, $7F=flash, $3F=inverse)", "scope": "workspace"},
    {"address": "0033", "name": "PROMPT", "description": "prompt character", "scope": "workspace"},
    {"address": "0034", "name": "YSAV", "description": "Y register save", "scope": "workspace"},
    {"address": "0035", "name": "YSAV1", "description": "Y register save (secondary)", "scope": "workspace"},
    {"address": "0036", "name": "CSWL", "description": "character output hook low", "scope": "workspace", "bytes": 2},
    {"address": "0037", "name": "CSWH", "description": "character output hook high", "scope": "workspace"},
    {"address": "0038", "name": "KSWL", "description": "character input hook low", "scope": "workspace", "bytes": 2},
    {"address": "0039", "name": "KSWH", "description": "character input hook high", "scope": "workspace"},
    {"address": "003A", "name": "PCL", "description": "program counter low", "scope": "workspace", "bytes": 2},
    {"address": "003B", "name": "PCH", "description": "program counter high", "scope": "workspace"},
    {"address": "003C", "name": "A1L", "description": "general purpose address 1 low", "scope": "workspace", "bytes": 2},
    {"address": "003D", "name": "A1H", "description": "general purpose address 1 high", "scope": "workspace"},
    {"address": "003E", "name": "A2L", "description": "general purpose address 2 low", "scope": "workspace", "bytes": 2},
    {"address": "003F", "name": "A2H", "description": "general purpose address 2 high", "scope": "workspace"},
    {"address": "0040", "name": "A3L", "description": "general purpose address 3 low", "scope": "workspace", "bytes": 2},
    {"address": "0041", "name": "A3H", "description": "general purpose address 3 high", "scope": "workspace"},
    {"address": "0042", "name": "A4L", "description": "general purpose address 4 low", "scope": "workspace", "bytes": 2},
    {"address": "0043", "name": "A4H", "description": "general purpose address 4 high", "scope": "workspace"},
    {"address": "0044", "name": "A5L", "description": "general purpose address 5; its high byte also saves the accumulator", "scope": "workspace", "bytes": 2},
    {"address": "0045", "name": "A5H / ACC", "description": "address 5 high / accumulator save (overlap)", "scope": "workspace"},
    {"address": "0046", "name": "XREG", "description": "X register save", "scope": "workspace"},
    {"address": "0047", "name": "YREG", "description": "Y register save", "scope": "workspace"},
    {"address": "0048", "name": "STATUS", "description": "processor status save", "scope": "workspace"},
    {"address": "0049", "name": "SPNT", "description": "stack pointer save", "scope": "workspace"},
    {"address": "004E", "name": "RNDL", "description": "random number low", "scope": "workspace", "bytes": 2},
    {"address": "004F", "name": "RNDH", "description": "random number high", "scope": "workspace"},
    {"address": "0200", "name": "INPUT_BUFFER", "description": "Input buffer (256 bytes)", "scope": "workspace"},
    {"address": "03F0", "name": "BRKV", "description": "BRK vector (2 bytes)", "scope": "workspace", "bytes": 2},
    {"address": "03F2", "name": "SOFTEV", "description": "Soft entry (warm start) vector (2 bytes)", "scope": "workspace", "bytes": 2},
    {"address": "03F4", "name": "PWREDUP", "description": "Power-up check byte (must = EOR #$A5 of SOFTEV+1)", "scope": "workspace"},
    {"address": "03F8", "name": "USRADR", "description": "USR() jump address (3 bytes)", "scope": "workspace"},
    {"address": "03FB", "name": "NMI", "description": "NMI vector on page 3 (3 bytes)", "scope": "workspace"},
    {"address": "03FE", "name": "IRQLOC", "description": "IRQ location vector (2 bytes)", "scope": "workspace", "bytes": 2},
    {"address": "C000", "name": "KBD", "description": "R: last key pressed + 128", "scope": "hardware"},
    {"address": "C010", "name": "KBDSTRB", "description": "RW: keyboard strobe (clear)", "scope": "hardware"},
    {"address": "C020", "name": "TAPEOUT", "description": "RW: toggle cassette tape output", "scope": "hardware"},
    {"address": "C030", "name": "SPKR", "description": "RW: toggle speaker", "scope": "hardware"},
    {"address": "C050", "name": "TXTCLR", "description": "RW: display graphics", "scope": "hardware"},
    {"address": "C051", "name": "TXTSET", "description": "RW: display text", "scope": "hardware"},
    {"address": "C052", "name": "MIXCLR", "description": "RW: display full screen", "scope": "hardware"},
    {"address": "C053", "name": "MIXSET", "description": "RW: display split screen", "scope": "hardware"},
    {"address": "C054", "name": "TXTPAGE1", "description": "RW: display page 1", "scope": "hardware"},
    {"address": "C055", "name": "TXTPAGE2", "description": "RW: display page 2", "scope": "hardware"},
    {"address": "C056", "name": "LORES", "description": "RW: display lo-res graphics", "scope": "hardware"},
    {"address": "C057", "name": "HIRES", "description": "RW: display hi-res graphics", "scope": "hardware"},
    {"address": "C058", "name": "SETAN0", "description": "RW: annunciator 0 off (TTL high)", "scope": "hardware"},
    {"address": "C059", "name": "CLRAN0", "description": "RW: annunciator 0 on (TTL low)", "scope": "hardware"},
    {"address": "C05A", "name": "SETAN1", "description": "RW: annunciator 1 off (TTL high)", "scope": "hardware"},
    {"address": "C05B", "name": "CLRAN1", "description": "RW: annunciator 1 on (TTL low)", "scope": "hardware"},
    {"address": "C05C", "name": "SETAN2", "description": "RW: annunciator 2 off (TTL high)", "scope": "hardware"},
    {"address": "C05D", "name": "CLRAN2", "description": "RW: annunciator 2 on (TTL low)", "scope": "hardware"},
    {"address": "C05E", "name": "SETAN3", "description": "RW: annunciator 3 off (TTL high)", "scope": "hardware"},
    {"address": "C05F", "name": "CLRAN3", "description": "RW: annunciator 3 on (TTL low)", "scope": "hardware"},
    {"address": "C060", "name": "TAPEIN", "description": "R: cassette input", "scope": "hardware"},
    {"address": "C064", "name": "PADDL0", "description": "R: analog input 0", "scope": "hardware"},
    {"address": "C065", "name": "PADDL1", "description": "R: analog input 1", "scope": "hardware"},
    {"address": "C066", "name": "PADDL2", "description": "R: analog input 2", "scope": "hardware"},
    {"address": "C067", "name": "PADDL3", "description": "R: analog input 3", "scope": "hardware"},
    {"address": "C070", "name": "PTRIG", "description": "RW: analog input reset (trigger)", "scope": "hardware"},
    {"address": "CFFF", "name": "CLRROM", "description": "Disable slot C8 ROM", "scope": "hardware"},
    {"address": "F962", "name": "FMT1", "description": "Instruction format table 1", "scope": "rom"},
    {"address": "F9A6", "name": "FMT2", "description": "Instruction format table 2", "scope": "rom"},
    {"address": "F9B4", "name": "CHAR1", "description": "Addressing mode character table 1", "scope": "rom"},
    {"address": "F9BA", "name": "CHAR2", "description": "Addressing mode character table 2", "scope": "rom"},
    {"address": "F9C0", "name": "MNEML", "description": "Mnemonic table (left bytes)", "scope": "rom"},
    {"address": "FA00", "name": "MNEMR", "description": "Mnemonic table (right bytes)", "scope": "rom"},
    {"address": "F80E", "name": "PLOT1", "description": "Continue plotting with the prepared screen pointer.", "scope": "rom"},
    {"address": "F81C", "name": "HLINE1", "description": "Continue the horizontal line loop.", "scope": "rom"},
    {"address": "FA9B", "name": "FIXSEV", "description": "Repair the warm-start vector.", "scope": "rom"},
    {"address": "FAA3", "name": "NOFIX", "description": "Continue with an existing warm-start vector.", "scope": "rom"},
    {"address": "FAA9", "name": "SETPG3", "description": "Initialize page-three vectors.", "scope": "rom"},
    {"address": "FABA", "name": "SLOOP", "description": "Search slot ROMs.", "scope": "rom"},
    {"address": "FAC7", "name": "NXTBYT", "description": "Check the next slot-ROM signature byte.", "scope": "rom"},
    {"address": "FB5B", "name": "TABV", "description": "Set CV from A, then recompute its screen pointer.", "scope": "rom"},
    {"address": "FBD0", "name": "BASCLC2", "description": "Complete text-row address calculation.", "scope": "rom"},
    {"address": "FBF4", "name": "ADVANCE", "description": "Advance the cursor after a character.", "scope": "rom"},
    {"address": "FBFD", "name": "VIDOUT", "description": "Dispatch the screen character handler.", "scope": "rom"},
    {"address": "FC10", "name": "BS", "description": "Move the cursor left.", "scope": "rom"},
    {"address": "FC1A", "name": "UP", "description": "Move the cursor up.", "scope": "rom"},
    {"address": "FC24", "name": "VTABZ", "description": "Continue vertical positioning.", "scope": "rom"},
    {"address": "FC76", "name": "SCRL1", "description": "Copy the next scrolling row.", "scope": "rom"},
    {"address": "FC9E", "name": "CLEOLZ", "description": "Clear the line starting at column Y.", "scope": "rom"},
    {"address": "FD35", "name": "RDCHAR", "description": "Process the input character.", "scope": "rom"},
    {"address": "FD67", "name": "GETLNZ", "description": "Read a line without first printing the prompt.", "scope": "rom"},
    {"address": "FD6F", "name": "GETLN1", "description": "Continue the line-input loop.", "scope": "rom"},
    {"address": "FD71", "name": "BCKSPC", "description": "Handle backspace during line input.", "scope": "rom"},
    {"address": "FD75", "name": "NXTCHAR", "description": "Read the next line-input character.", "scope": "rom"},
    {"address": "FD84", "name": "ADDINP", "description": "Store the line-input character.", "scope": "rom"},
    {"address": "FDF6", "name": "COUTZ", "description": "Continue the screen output handler.", "scope": "rom"},
    {"address": "FE75", "name": "A1PC", "description": "Copy the Monitor address into its execution pointer.", "scope": "rom"},
    {"address": "FF69", "name": "MONZ", "description": "Read and dispatch a Monitor command.", "scope": "rom"}
  ],
  "notes": [
    {"address": "FDED", "bytes": "6C 36 00", "text": "Dispatch through CSWL/CSWH; the hook chooses where output goes."},
    {"address": "FDF4", "bytes": "25 32", "text": "Apply INVFLG to printable output before the screen handler."},
    {"address": "FBF0", "bytes": "A4 24", "text": "Load the column within the text window into Y."},
    {"address": "FBF2", "bytes": "91 28", "text": "Store the character at the row pointer plus the cursor column."},
    {"address": "FBF4", "bytes": "E6 24", "text": "Advance the cursor one column after storing the character."},
    {"address": "FBF8", "bytes": "C5 21", "text": "Compare the new column with the text-window width."},
    {"address": "FBFA", "bytes": "B0 66", "text": "At the right edge, take the carriage-return path."},
    {"address": "FC62", "bytes": "A9 00", "text": "Prepare column zero: the left edge of the text window."},
    {"address": "FC64", "bytes": "85 24", "text": "Return the cursor column to zero; no screen byte is written."},
    {"address": "FC66", "bytes": "E6 25", "text": "Advance the cursor row."},
    {"address": "FC6A", "bytes": "C5 23", "text": "Compare the new row with the exclusive bottom boundary."},
    {"address": "FC6C", "bytes": "90 B6", "text": "Below the bottom boundary, recalculate the row pointer."},
    {"address": "FC6E", "bytes": "C6 25", "text": "Keep CV on the last row before scrolling the window."},
    {"address": "FC70", "bytes": "A5 22", "text": "Start scrolling at the top row of the text window."},
    {"address": "FC78", "bytes": "85 2A", "text": "Save the destination row pointer low byte in BAS2L."},
    {"address": "FC7C", "bytes": "85 2B", "text": "Save the destination row pointer high byte in BAS2H."},
    {"address": "FC7E", "bytes": "A4 21", "text": "Start at the window width; DEY selects its last column."},
    {"address": "FC89", "bytes": "20 24 FC", "text": "Calculate the next row pointer to use as the copy source."},
    {"address": "FC8C", "bytes": "B1 28", "text": "Read a character from the following screen row."},
    {"address": "FC8E", "bytes": "91 2A", "text": "Copy that character into the preceding screen row."},
    {"address": "FC90", "bytes": "88", "text": "Move left to the preceding column."},
    {"address": "FC91", "bytes": "10 F9", "text": "Keep copying columns while Y is non-negative."},
    {"address": "FC95", "bytes": "A0 00", "text": "Begin clearing the bottom row at its first column."},
    {"address": "FC9E", "bytes": "A9 A0", "text": "Use a normal space as the clearing byte."},
    {"address": "FCA0", "bytes": "91 28", "text": "Clear a cell through the current row pointer."}
  ],
  "walkthroughs": [
    {
      "id": "echo",
      "title": "From reset to an echoed A",
      "setup": "Load the verified ROM, eject any disk, and use Fresh power-on. Disable instruction breakpoints and watchpoint stops that would interrupt these checkpoints. Leave Screen Inspect off when typing.",
      "steps": [
        {
          "title": "The reset vector",
          "address": "FA62",
          "prepare": "Fresh power-on pauses here before any ordinary instruction.",
          "observe": "Step executes CLD, clearing decimal mode. The CPU read the reset vector to obtain this address.",
          "routine": "RESET"
        },
        {
          "title": "Clear the text window",
          "address": "FC58",
          "prepare": "Run to this checkpoint from reset.",
          "observe": "HOME will clear the text window and position its cursor. The screen changes because the ROM writes RAM.",
          "routine": "HOME"
        },
        {
          "title": "The output hook",
          "address": "FDED",
          "prepare": "Run to this checkpoint to stop before the first output dispatch.",
          "observe": "Step follows JMP ($0036). The output hook is a RAM word, not a destination hard-coded in the CPU.",
          "routine": "COUT"
        },
        {
          "title": "Ready for a key",
          "address": "FD21",
          "prepare": "Run to this checkpoint. The screen should show the Apple banner and an empty prompt.",
          "observe": "Type A without Enter, then Step twice: BIT $C000 sees $C1 and sets N; BPL falls through. Typing only queues input until execution resumes.",
          "routine": "KEYIN2"
        },
        {
          "title": "Acknowledge the key",
          "address": "FD2B",
          "prepare": "After those two steps, run to this checkpoint.",
          "observe": "A holds $C1. Step executes BIT $C010, clearing the keyboard strobe.",
          "routine": "Acknowledge key"
        },
        {
          "title": "Store the echoed character",
          "address": "FBF2",
          "prepare": "Run to this checkpoint. STORADV has loaded CH into Y.",
          "observe": "For this fresh prompt, CH is 1 and BASL/BASH is $0500. Step writes $C1 to $0501. Turn on Screen Inspect and select row 2, column 1 to see this instruction as its writer.",
          "routine": "STORADV"
        },
        {
          "title": "Wait for another key",
          "address": "FD21",
          "prepare": "Run to this checkpoint after the store.",
          "observe": "The display shows ]A. This is line editing and echo; no command has been submitted.",
          "routine": "KEYIN2"
        }
      ]
    },
    {
      "id": "carriage-return",
      "title": "Carriage return and cursor movement",
      "setup": "Start again with Fresh power-on, no disk, and no enabled breakpoint/watchpoint stops. This experiment uses an empty prompt; do not submit the A from the echo walkthrough.",
      "steps": [
        {
          "title": "An empty prompt",
          "address": "FD21",
          "prepare": "Run to this checkpoint from fresh power-on.",
          "observe": "The cursor is on row 2. With Inspect off, press Enter, then select the next checkpoint.",
          "routine": "KEYIN2"
        },
        {
          "title": "Dispatch a control character",
          "address": "FDED",
          "prepare": "After queuing Enter, run to this checkpoint.",
          "observe": "A is $8D. COUT sends control characters through the same output hook as printable characters.",
          "routine": "COUT"
        },
        {
          "title": "Return to the left edge",
          "address": "FC62",
          "prepare": "Run to this checkpoint.",
          "observe": "Two steps execute LDA #$00 and STA $24. CH becomes zero without writing a screen character.",
          "routine": "CR"
        },
        {
          "title": "Advance the row",
          "address": "FC66",
          "prepare": "After those two steps, this is the current address.",
          "observe": "Step increments CV from 2 to 3. The following comparison decides whether the ROM must scroll.",
          "routine": "LF"
        },
        {
          "title": "Back at the prompt",
          "address": "FD21",
          "prepare": "Run to this checkpoint.",
          "observe": "CV is now 4: the Applesoft path back to the prompt emitted another newline after the echoed carriage return. A routine’s effect differs from the whole input path.",
          "routine": "KEYIN2"
        }
      ]
    },
    {
      "id": "scroll",
      "title": "How the ROM scrolls the screen",
      "setup": "Start at the fresh empty prompt on row 4 at the end of the carriage-return walkthrough. With Inspect off, repeat Enter, Step once, then Run to the first checkpoint nine times to reach row 22. Disable breakpoint/watchpoint stops for an uninterrupted walkthrough.",
      "steps": [
        {
          "title": "Prepare the bottom of the window",
          "address": "FD21",
          "prepare": "At each prompt, Step once after Enter before running here again; run-to stops immediately if already at its target. After nine empty lines from row 4, queue Enter once more and select the next checkpoint.",
          "observe": "CH is 1 and CV is 22 before that final Enter. WNDTOP is 0, WNDWDTH is $28 (40), and WNDBTM is $18 (24).",
          "routine": "KEYIN2"
        },
        {
          "title": "Enter the scroll routine",
          "address": "FC70",
          "prepare": "With the final Enter queued, run to this checkpoint.",
          "observe": "CV is held at 23. The ROM will copy the next row over each preceding row within the window.",
          "routine": "SCROLL"
        },
        {
          "title": "Copy one character",
          "address": "FC8E",
          "prepare": "Run to this checkpoint. The preceding load fetched a character from the next row.",
          "observe": "Y is 39; the source is $04A7 and destination $0427. Inspect row 0, column 39, then Step. The last writer becomes $FC8E even when a space replaces a space.",
          "routine": "SCROLL"
        },
        {
          "title": "Rows have moved",
          "address": "FC95",
          "prepare": "Run to this checkpoint to finish the copy loops.",
          "observe": "Rows 0–22 now contain the old rows 1–23. Their last writer is the copy instruction at $FC8E.",
          "routine": "SCROLL"
        },
        {
          "title": "Clear the last row",
          "address": "FCA0",
          "prepare": "Run to this checkpoint.",
          "observe": "A is $A0, a normal space, and Y is zero. Step begins clearing the bottom row at $07D0. The same store is also used for other clearing operations.",
          "routine": "CLREOL"
        },
        {
          "title": "The prompt at the bottom",
          "address": "FD21",
          "prepare": "Run to this checkpoint.",
          "observe": "The machine is waiting on row 23. Scrolling came from ordinary ROM loads and stores; the browser rendered the resulting RAM.",
          "routine": "KEYIN2"
        }
      ]
    },
    {
      "id": "cursor-edit",
      "title": "Move the cursor by editing RAM",
      "setup": "Use the laboratory. Load the verified ROM, eject any disk, use Fresh power-on, and disable breakpoint/watchpoint stops. Leave Screen Inspect off for typing. This experiment edits one Monitor workspace byte, then restores a saved state.",
      "steps": [
        {
          "title": "Save the empty prompt",
          "address": "FD21",
          "prepare": "Run to this checkpoint from fresh power-on. In Saved states, save a state named Cursor before move, without typing a key.",
          "observe": "Zero page → Names shows CH = $01, CV = $02, and BASL/BASH = $0500. The saved state includes RAM, CPU, devices, and the empty keyboard queue.",
          "routine": "KEYIN2"
        },
        {
          "title": "Change the cursor column",
          "address": "FD21",
          "prepare": "Stay paused here. In Zero page, turn EDIT on. In the CH row, select the byte beside Bytes, enter 08, and Apply. Turn EDIT off. In Saved states, Compare with Cursor before move.",
          "observe": "Only RAM $0024 differs: $01 → $08. No instruction ran and Y is still $01. Changes labels the action User RAM edit; earlier execution history has been cleared. Columns count from zero.",
          "routine": "KEYIN2"
        },
        {
          "title": "Reload the changed column",
          "address": "FBF0",
          "prepare": "Click the screen and type A without Enter. Run to this checkpoint.",
          "observe": "STORADV starts with LDY $24. Step loads the edited CH into Y, making it $08. The keyboard loop restored its old cursor cell before reaching this routine.",
          "routine": "STORADV"
        },
        {
          "title": "Store at column eight",
          "address": "FBF2",
          "prepare": "After that step, this is the current address. Step once to execute STA ($28),Y.",
          "observe": "The ROM writes $C1 to $0508: row base $0500 plus column $08. The A appears at row 2, column 8. Compare again to see the accumulated effects; this comparison is a net difference, not an instruction trace.",
          "routine": "STORADV"
        },
        {
          "title": "Restore and repeat",
          "address": "FD21",
          "prepare": "In Saved states, Restore Cursor before move. Stay paused: Restore itself returns here. Compare again, then type A and repeat the last two checkpoints without editing CH.",
          "observe": "After Restore, Compare shows no differences and CH is $01 again. Repeating the echo writes $C1 to $0501. The same ROM code uses the workspace supplied to it; the emulator did not special-case the character or cursor.",
          "routine": "KEYIN2"
        }
      ]
    },
    {
      "id": "output-hook",
      "title": "Redirect the ROM's character output",
      "setup": "Use the laboratory with the verified ROM, no disk, and Fresh power-on. Disable breakpoint/watchpoint stops and leave Screen Inspect off for typing. Save before editing: this experiment replaces a ROM output hook with a four-byte RAM routine.",
      "steps": [
        {
          "title": "Save the original output hook",
          "address": "FD21",
          "prepare": "Before running from fresh power-on, add a WORD watch at $0036 and a BYTE watch at $0310, with stops off. Run to this checkpoint. Save a state named Output hook original. In Zero page → Names, Watch BASL/BASH too.",
          "observe": "CSWL/CSWH = $FDF0 and BASL/BASH = $0500. History beside CSWL/CSWH shows $FEA9 writing $F0 to $0036 and $FEAB writing $FD to $0037. Browse an instruction or follow a word's value to Memory, then use the header's Back/Forward arrows to return. Browsing does not run the machine.",
          "routine": "KEYIN2"
        },
        {
          "title": "Install a small output handler",
          "address": "FD21",
          "prepare": "Stay paused. Use Memory EDIT to write $0300: 8D 10 03 60 (four separate bytes). This is STA $0310 followed by RTS. Then set $0036: 00 03 (low byte, then high byte). Turn EDIT off after applying all six bytes.",
          "observe": "CSWL/CSWH now reads $0300. Follow that word to Memory and select MEM in Disassembly to see STA $0310 and RTS. The watches remain, but edits cleared their old history. The handler will capture A at $0310 rather than send it to the screen. No ROM bytes have changed.",
          "routine": "KEYIN2"
        },
        {
          "title": "Follow the indirect output jump",
          "address": "FDED",
          "prepare": "Click the screen and type A without Enter. Run to this checkpoint, then Step once.",
          "observe": "COUT executes JMP ($0036): History for CSWL/CSWH shows two reads by $FDED, $00 from $0036 then $03 from $0037. PC reaches $0300 and A holds $C1. The jump does not push a new return address; SP is unchanged. Select PC in Disassembly to follow execution again after browsing.",
          "routine": "COUT"
        },
        {
          "title": "Capture the character and return",
          "address": "FD21",
          "prepare": "From $0300, Step once to store A at $0310, then Step once for RTS. Inspect those two history records before running to this checkpoint.",
          "observe": "History for $0310 identifies $0300's STA: RAM $0310 changed $00 → $C1. Its instruction link browses the handler; Back returns to the previous code address. RTS uses the existing caller's return and continues at $FD4A; SP increases by two. Back at the keyboard loop, CH is still $01 and no A was echoed, although the line editor accepted it. Compare with the saved state; do not press Enter.",
          "routine": "KEYIN2"
        },
        {
          "title": "Restore normal screen output",
          "address": "FDED",
          "prepare": "Restore Output hook original, then Compare: there should be no differences. Type A without Enter and run to this checkpoint. Step once, then run to the first checkpoint to finish the echo.",
          "observe": "The restored CSWL/CSWH is $FDF0. Its fresh History shows $FDED reading $F0 then $FD; the same JMP now enters COUT1, and the prompt becomes ]A. Restore removed the RAM handler, captured byte, and prior execution history. Watch preferences and inspector navigation remain part of the workspace, independent of the saved machine.",
          "routine": "COUT"
        }
      ]
    }
  ]
}

The labels were checked against the Apple II Reference Manual's autostart listing, available as a searchable disassembly, and the pinned dromaios-apple2 ROM annotations. The walkthrough's register, keyboard, screen, and stopping behavior is tested by executing the selected ROM; no firmware image is distributed with this guide. The 6502 specification supplies the instruction names and addressing notation used in the explorer.

View the walkthrough source ↗

The Apple II Plus pairs a MOS 6502 with memory-mapped input and display hardware. Unlike the Altair's serial terminal, its text output lives in RAM, where video hardware fetches character codes. Applesoft and the Autostart Monitor occupy the upper 12 KiB of the address space.

This is the first native composition in the Apple II Plus plan: 48 KiB of main RAM, caller-supplied firmware, the keyboard latch, and text and graphics displays, plus a 16 KiB Language Card and an optional slot-6 Disk II. It runs BASIC and boots the selected DOS 3.3 System Master from a local, read-only disk image. Disk writes and sound remain future work. The main reference is the pinned dromaios-apple2 implementation; Apple's Reference Manual supplies the hardware background.

Components and address decoding

Main RAM fills 0000–BFFF; page-one text uses 0400–07FF within that same storage. The keyboard chapter owns character/strobe state and all C000–C01F aliases. The video chapter owns C050–C057 display switches, interleaved display addresses, character attributes, and low- and high-resolution colour decoding. The Language Card chapter owns C080–C08F selection and write-protection latches. Two 4 KiB RAM banks share D000–DFFF, followed by 8 KiB of common RAM at E000–FFFF. ROM supplies reads at power-on; RAM can receive writes behind it. The windows below select the first matching rule independently for reads and writes. The common upper window includes interrupt and reset vectors, so enabling RAM reads also selects the vectors stored in RAM. Disk II uses C0E0–C0EF for control and C600–C6FF for its bootstrap. The device starts uninstalled: without a host-supplied bootstrap, both windows are unanswered and the Monitor falls through to Applesoft.

components {
    ram = ram C000
    keyboard = apple2-keyboard
    video = apple2-video
    firmware = rom 3000
    language = apple2-language-card
    bank1 = ram 1000
    bank2 = ram 1000
    upper = ram 2000
    disk = apple2-disk-ii
}

map 10000 {
    0000 = ram
    C000 = keyboard
    C050 = video
    C080 = language
    C0E0 = window 10 {
        read = disk
        write = disk
    }
    C600 = window 100 {
        read = disk offset 100
        write = discard
    }
    D000 = window 1000 {
        read = bank2 when language.ramRead and language.bank2
        read = bank1 when language.ramRead
        read = firmware
        write = bank2 when language.ramWrite and language.bank2
        write = bank1 when language.ramWrite
        write = discard
    }
    E000 = window 2000 {
        read = upper when language.ramRead
        read = firmware offset 1000
        write = upper when language.ramWrite
        write = discard
    }
    unmapped = 00
}

Unanswered reads return 00 and unanswered writes are discarded. Protected Language Card writes are discarded; enabled writes change the selected RAM even while reads still see ROM. Firmware bytes never change. This explicit byte-bus policy is an approximation, not a model of the Apple II video scanner's floating bus. Unimplemented soft switches have no effect. Programs that depend on them are outside this initial composition.

Firmware supplied by the caller

The executable declaration identifies the exact 12,288 bytes mapped at D000. No firmware bytes are downloaded, generated, or committed here.

image firmware external sha256 378ba00c86a64cca49cedaca7de8d5d351983ebc295d9d11e0752febfc346249

The generated module exports romImages.firmware (size and SHA-256). The host uses RomImage.verify with its SHA-256 implementation before calling create6502Apple2({ firmware }). To allocate hardware before firmware is available, use create6502Apple2({ firmware: null }). RAM and devices exist immediately; machine.firmware.loaded is false. The empty ROM connection returns an unanswered transfer, which the declared bus resolves to 00. This is absence of firmware, not a stored zero-filled ROM image.

After verification, machine.firmware.install(firmware) installs an owned, immutable image without replacing RAM, resetting the CPU, or changing devices. Call machine.reset() separately to read the reset vector and prepare to boot. Factories remain synchronous. These operations are also described in the external ROM contract.

The reference's roms/apple2p.rom is a container. This record gives its byte count, SHA-256, and the decimal offset of the mapped ROM, followed by the Disk II bootstrap slice and selected DOS disk identity. The browser uses this record directly; none of these files is bundled with the site:

{
  "bytes": 20480,
  "sha256": "92c4bef609920842ea472d21b661a0d35dbda6cd90963b8b734a205e22d84108",
  "offset": 8192,
  "bootstrap": {
    "bytes": 256,
    "sha256": "de1e3e035878bab43d0af8fe38f5839c527e9548647036598ee6fe7ec74d2a7d",
    "offset": 1536
  },
  "disk": {
    "bytes": 143360,
    "sha256": "06075b2b73922cfa292c5c36de5a27b17f9fbf21ef0450a3dc00b53a6bf55d17"
  }
}

A host accepting that file must validate the whole file before extracting hexadecimal offsets 2000–4FFF, then verify the normalized image above. The exact 12 KiB image is also accepted directly. Other sizes or altered files are rejected before replacing an existing machine.

Disk boot also extracts the 256-byte P5 bootstrap at container offsets 0600–06FF and verifies its separate digest. The browser's disk option requires this container; a bare 12 KiB motherboard image still supports ROM-only use. The selected disk is the reference's disks/dos33-master.dsk. Its size and whole-file digest above are checked before insertion. The host calls disk.install(bootstrap) and disk.insert(new Dos33Disk(bytes)); those focused device methods own copies of their inputs. The generated machine needs no Disk II-specific construction code.

Construction, reset, and restoration

Construction provides zero-filled RAM and deterministic CPU, keyboard, display, Language Card, and disk-controller state. It does not execute or reset the CPU. Call reset() to perform the processor's real reset-vector reads at FFFC and FFFD; at power-on these read ROM, which points to FA62. Later resets follow the card's current RAM/ROM selection. The initial PC below is not a shortcut into the ROM.

cpu 6502 {
    A = 00  X = 00  Y = 00
    PC = 0000  SP = FF
    flags { N = 0  V = 0  D = 0  I = 0  Z = 0  C = 0 }
}

reset { cpu }

CPU reset preserves RAM, firmware, the keyboard latch, display switches, all Language Card latches, and the disk controller, head, and stream position. It follows the 6502 reset contract, including its stack-pointer and interrupt-mask changes. Firmware may then clear the strobe and initialize its own workspace. Fresh construction, followed by reset, represents power-on in this deterministic model.

snapshot() captures CPU, all four RAM components, keyboard, display and Language Card latches, the disk controller with its bootstrap and media bytes, and the firmware digest (or null when not installed). Restoration requires a matching binding: the same verified image for a digest, or null for absence. It creates independent hardware without resetting or executing. Snapshot inspection performs no guest device reads. There is no generic safe preview of mapped I/O: inspect device snapshots and read-only views, and read RAM or ROM directly instead.

Using the browser machine

The classroom presents this guide alongside an embedded machine. Its laboratory link opens a separate dark workspace with the screen, CPU and instruction preview, execution history, memory, stack, and Disassembly/Changes/ROM/System/Disk tools. Panels can be moved, resized, grouped into tabs, or collapsed to their headers. Moving a standalone panel within a row or column reorders it without changing the panels' sizes. Drop between panels at the insertion line, or use Arrange panels with Above, Below, Left of, or Right of. Each divider resizes only its two neighbours. Closing a panel hides its tool; it does not stop the machine or its capture. Reset layout restores the arrangement while preserving the current experiment. Each inspector header has a Live toggle: a green square enables automatic updates during Run; an empty square holds that view. Pause, Step, and opening a panel refresh it regardless of Live. Its own navigation and filtering controls also refresh on demand. These switches never stop execution or history capture, and are remembered separately from the layout. All inspectors start Live except Memory. The screen and machine/media controls always stay current. The Execution panel contains Run, Pause, Step, Reset CPU, Fresh power-on, the instruction count, and status messages. Below Screen, it shares a narrow column with MOS 6502 and Instruction; Disassembly spans the height beside them. It can be moved, collapsed, or reopened through Panels, like the other tools. The screen accepts typing and pasting directly, including Enter and Ctrl-C; the classroom also has an explicit keyboard field. Disassembly owns run-to-address and run-to-routine controls; ROM owns firmware selection and its searchable Monitor reference, with PC/Memory following and navigation to the other inspectors. Browsing a reference entry never executes it; the ROM guide owns the annotation and mapping contracts. Both use the same machine implementation, but each page starts its own session. Hardware is created immediately, before any ROM selection. RAM, stack memory, registers, and device state are inspectable in their deterministic initial state: RAM is zero, PC is 0000, and SP is FF; no reset has occurred. The screen also reflects this uninitialized text RAM: zero is an inverse @ character, until firmware fills the display. The browser's Run, Step, keyboard, and Reset CPU controls wait for verified firmware; Fresh power-on is available without it. This is a host boot workflow, not a hardware requirement for executing code in RAM. The laboratory's memory and Disassembly views read mapped storage without operating devices; -- denotes unavailable bytes, including soft switches. Disassembly's PC / MEM header toggle selects the next instruction or Memory's selected address, while Execution history preserves the bytes actually fetched. The live disassembly guide explains navigation and address stops.

Memory's FIX / PC / CHG header toggle controls its position. FIX starts at a fixed address. PC keeps the next instruction's address visible; CHG follows the last byte of visible RAM actually changed by an executed instruction, including earlier instructions in a running batch and writes captured while Changes recording is off. Hidden Language Card writes, unchanged stores, and device accesses do not move it. A later bank switch can hide its last target; the inspector waits for a visible changed byte. Zero page, Memory, and the full stack page each have a compact 8 / 16 header toggle for bytes per row, remembered separately. Reformatting preserves held values and change highlights. Memory scrolls through the complete 64K address space, rendering only the visible rows and a small margin. Its header address field selects a byte on Enter; scrolling selects the first visible row. Both return to Fixed. With Live off, even newly scrolled rows come from the same captured storage image; scrolling does not read newer machine values.

Following moves the viewport only when the target leaves it, aligning the new start to the selected row width and stopping at FFFF. Following controls position independently of Live: with Live off, the viewport catches up when paused, stepped, or explicitly refreshed. Reset, fresh power-on, and successful media replacement forget the last change target but keep the following mode; fixed views retain their address. Position and mode last for this visit.

Stack shows one byte per line from SP+1 through 01FF, with the next pull marked. This is a conventional view above SP, not a claim that the 6502 tracks stack depth: wrapping and software writes can use any byte in page 01. PAGE shows all 256 bytes, including the next push slot. The status gives both push and pull addresses, wrapping the pull within page 01.

Instruction previews the next instruction's register and flag changes, write requests, destination PC, and ordered memory accesses. For conditional branches it names the flag and required value, shows the current value, and explains whether the branch is taken. Conditions come from the CPU chapter; BEQ tests Z, without assuming which earlier instruction set it. It runs a copy of the generated CPU against storage observations and private RAM writes; it never executes on the guest machine. Device or unavailable reads stop the preview with an explicit explanation. Writes to mapped devices are shown as requests, without predicting peripheral effects. This keeps instruction behavior owned by the CPU specification while Execution history describes actual execution.

Execution history lists the latest twelve captured instructions, newest first, and shows the selected instruction's register/flag changes and ordered memory accesses. Follow latest initially selects the newest step. Selecting an entry pins its record, including when it leaves the recent twelve; the view labels that case and retains only that one extra record. Follow latest resumes tracking. Selection never executes an instruction or changes the live inspectors. Reset, fresh power-on, and successful media replacement clear this selection and history; failed replacements leave them intact. Execution history and Changes start in one tab group, but either can be docked separately.

Register and flag highlights compare the last completed instruction's recorded before-and-after values, including flags cleared to zero. They remain visible while paused, with the old value on hover, until the next instruction replaces them. While running, only the latest completed instruction is represented. Reset, fresh power-on, and an execution error clear the highlights; the values themselves always show the current processor state.

The laboratory's zero page, stack page, and memory window use the same last-instruction highlight and previous-value tooltip for changed RAM bytes. Overlapping windows highlight the same byte consistently. The highlights remain when browsing memory or clearing or pausing the log, and are replaced by the next instruction. They reflect actual stores to the visible RAM bank; a bank switch alone does not highlight newly visible bytes, and writes behind ROM do not highlight the ROM. Multiple stores to one byte compare its first before-value with its final value, so a byte restored by the instruction is not highlighted. Reset, fresh power-on, and execution errors clear these highlights along with those on the processor.

The laboratory's Changes records every instruction while Record changes is enabled, including steps between display refreshes and while another tab is selected. It retains the latest 500 events (instructions or explicit RAM edits), newest first, and reports how many older events were discarded. Each CPU group identifies the instruction's starting address and fetched bytes decoded as assembly. Register, flag, and PC changes compare instruction boundaries; memory changes preserve the order of actual stores. Values are hexadecimal, with flags shown as 0 or 1. RAM observation captures the old byte at the store itself, without a guest read. Language Card bank 1, bank 2, and common upper RAM are labeled separately, even when writes occur behind mapped ROM. Unchanged stores, ignored ROM writes, and device accesses are not RAM changes; device state remains in System. Filters change only the display, not capture. Pausing recording preserves the history while execution continues; instruction numbers leave gaps for unrecorded steps. Clear log restarts numbering. Reset, fresh power-on, and successful ROM or disk replacement clear the history. A failed replacement preserves it. Execution errors retain completed effects, marked as interrupted if no complete CPU record was returned; they are not presented as successful instructions.

The browser also provides Explore the ROM, with recorded instructions, register and flag changes, memory accesses, and one-shot address or ROM routine stops. Follow the ROM walkthrough from reset to the Applesoft prompt, then trace a keypress to its screen write. The software guide owns the versioned labels and walkthrough; these do not change the machine's wiring or firmware execution.

Use Load ROM… to choose the matching local file, then Run. The file picker stays hidden behind the button; the separate loaded-file status identifies the installed ROM, including one restored from browser storage. Once loaded, the button becomes Replace ROM…. Disk selection uses the same Load/Replace pattern. Canceling a picker or rejecting an invalid file retains the loaded media. Nothing is uploaded or fetched. The browser remembers the verified file in local storage, including the Disk II bootstrap when the selected file is the 20 KiB container. On later visits, both the classroom and laboratory reverify that saved copy and prepare a fresh, paused machine automatically. This is shared within the same browser and site; the local preview and public site have separate storage. Forget saved ROM removes that copy without unloading firmware from an already open machine. If storage is unavailable or the saved copy fails verification, a message explains the failure and you can still choose a local file. A rejected file selection does not replace the saved ROM.

Wait for the APPLE ][ banner and Applesoft's ] prompt, click the keyboard field, and type PRINT 2+3, then Enter. The reply is 5. The display reads the same RAM the CPU writes; output does not pass through a host BASIC interpreter.

Try a stored program, entering one line at a time and waiting for the next prompt:

20 END
10 PRINT 2+3
LIST
RUN

LIST puts line 10 before line 20; RUN prints 5. Lowercase letters become uppercase before delivery to the original seven-bit keyboard. Backspace or Left Arrow backs up over a character; Right Arrow reuses the character under the cursor. Enter and Control-C have buttons as well as keyboard shortcuts. Escape is passed to the firmware. Paste accepts one ASCII line with an optional final newline; multiple lines and non-ASCII text are rejected as a whole. There is no local echo: the ROM decides how keys affect the screen.

The host queues at most 4,096 characters and offers them through the hardware latch. Queued keys are delivered only during execution. Pause freezes the CPU and flashing phase; Step executes one instruction. Run resumes in bounded batches, with no claim of original-machine speed. Hiding the page pauses it; returning requires Run. Inspection reads CPU records, snapshots, and RAM without acknowledging keys or changing display switches.

Reset CPU discards the host input queue and performs the reset-vector reads, preserving RAM and device latches immediately. The firmware's subsequent actions are separate from that reset. Fresh power-on replaces the machine with empty RAM and fresh devices using the retained verified ROM and selected disk; the old program is lost. A failed ROM selection preserves the previous machine, paused. A successful first selection installs firmware into the existing hardware, then resets the CPU without changing RAM or devices. Selecting a ROM again prepares a fresh, paused machine, as does disk selection. Refreshing or leaving the page loses the current experiment unless you explicitly save it in the laboratory. The ROM is remembered independently; saved states never resume automatically.

For DOS, open Boot DOS 3.3 from disk and choose the matching local image. A successful selection replaces the running machine with a fresh, paused disk configuration. A rejected file preserves the previous machine, paused. Run then boots DOS; Fresh power-on retains the selected disk and starts it again. Eject disk pauses and removes the medium while preserving RAM and controller latches. Run can continue resident software, but disk reads cannot complete without media. Fresh power-on after eject returns to ROM-only Applesoft. Replacing the ROM clears the disk selection. All media stays on your computer.

Both text pages support normal, inverse, and flashing characters, using the same bitmap character set as dromaios-apple2. Text remains selectable. The Scanlines control toggles the display effect, including while paused. Monochrome switches text and graphics to a green display; both controls work independently, redraw while paused, and leave the machine state unchanged. Low-resolution graphics draws coloured blocks; high-resolution graphics uses the reference's approximate colour pairs. Mixed mode keeps four text rows. The video chapter defines those choices, the RGB palettes, and host flashing.

Editing RAM

In the laboratory, EDIT in Memory, Zero page, or Stack enables byte editing while paused. Select a byte, enter a hexadecimal value from 00 to FF (an optional $ is accepted), and choose Apply or press Enter. Cancel or Escape discards the draft. In named Zero page, Bytes exposes each byte of a workspace value, low byte first. Outside Edit mode, byte clicks retain their usual watch action; the explicit Watch links also remain available in Names.

Edits write the physical RAM currently displayed: main RAM, the selected Language Card bank, or its common upper RAM. ROM, bootstrap, and device addresses are read-only. A debugger edit can change visible Language Card RAM even when CPU writes are protected; it neither changes that protection nor operates the guest bus. The editor rejects a draft whose byte, mapped bank, or machine has changed. Starting Run or selecting media leaves Edit mode and cancels its draft. RAM editing is available even without firmware.

A changed byte clears execution history, observed calls, device activity, temporary execution targets, and screen-writer attribution, and restarts the instruction count. Changes starts a new history with User RAM edit, naming the physical bank and before/after values without inventing an instruction address. This explicit action is recorded even with instruction recording off. An unchanged value has no effect on history. Other RAM, registers, device latches, queued keyboard input, and saved states are preserved. Save an experiment first to restore it after editing; the cursor walkthrough demonstrates this with the Monitor's CH workspace byte.

Saving and comparing an experiment

The laboratory's Saved states panel stores named experiments in this browser and site origin. Pause, give the state a name, and choose Save new state. Each state includes the complete hardware snapshot, the host keyboard queue, selected disk name, and paused flashing phase. Main RAM, both Language Card banks and common upper RAM are included even when hidden by the current mapping. Panel arrangement, watches, breakpoints, and display preferences belong to the workspace and are not part of the saved machine.

Restore checks the saved machine version, complete component state, firmware identity, and any stored bootstrap/disk identities before replacing the current experiment. Load the matching verified ROM first; its bytes are not duplicated inside a saved state. Restoration executes no instruction or reset and always leaves the machine paused. It clears executed-instruction history, the Changes log, observed calls, device activity, last-writer attribution, and temporary run-to requests. The instruction counter restarts at zero. Layout and saved breakpoint/watchpoint preferences remain; Fresh power-on uses the restored experiment's media selection. Invalid data or failed verification leaves the current machine and its queued input untouched.

Compare captures saved → current differences while paused: registers, flags, physical RAM bytes, device fields, and the host input queue. It does not change either machine. This is a net comparison, so a byte changed and then restored to its saved value is absent. Memory links browse the current mapped bank; a link to hidden Language Card RAM explains the mismatch without changing mapping. Large differences show 128 RAM bytes at a time, with Show more for the rest. The result stays a captured comparison until Compare is pressed again; replacing the machine clears it. Use the Changes log for instruction attribution.

Saved states survive reloads but do not replace the machine until Restore is pressed. Names must be unique. Delete saved state removes only the selected saved copy; it does not affect the current machine. A failed save reports the storage problem and retains existing states. Browser storage limits apply, especially to states containing disk images. Clearing this site's browser data removes the states. Incompatible future machine versions are rejected rather than partially restored.

A repeatable ROM experiment: stop at KEYIN2 (FD21), type A while paused, and save before executing BIT. Step twice, then Compare: the queue has delivered its character, the keyboard strobe is set, flags reflect BIT, and PC has passed the branch. Restore, then repeat those steps; the same results should appear. A reload followed by Restore must reproduce the same continuation.

Drawing with Applesoft

At the Applesoft prompt, type NEW to clear the earlier program, then enter these lines one at a time. RUN draws sixteen horizontal colour bands, a white vertical line at the lower left, and a white block at the lower right:

10 GR
20 FOR C=0 TO 15
30 COLOR=C
40 HLIN 0,39 AT C*2
50 HLIN 0,39 AT C*2+1
60 NEXT C
70 COLOR=15
80 VLIN 32,39 AT 0
90 PLOT 39,39
100 PRINT "16 COLOURS"
110 END

GR selects the primary page in mixed low-resolution mode and clears its 40-by-40 graphics area. Each byte holds two blocks, so the two HLIN commands fill one complete byte row with the chosen colour. VLIN and PLOT alter only the selected blocks. The bottom four text rows show the caption and prompt. The acceptance test enters this exact fenced program into the real interpreter and checks every graphics block, with no drawing commands executed by the host.

TEXT returns to text mode. Switching does not erase graphics memory: its bytes can become unusual characters until the firmware overwrites them. Pause and Step work in graphics as in text; observing a frame changes no guest state.

The hardware supports a second page and full-screen low-resolution graphics. BASIC's ordinary drawing routines still use the primary page; changing the PAGE2 display switch alone does not redirect their writes. Page two overlaps Applesoft program storage, so arbitrary POKEs there can damage the program. The checks temporarily change 0800 (the leading zero byte required by Applesoft) and 0BF7 (beyond this short program), select the second page through C055, and verify the upper and lower nibbles. C052 then reveals the bottom eight block rows; C054 and C053 restore page one and mixed mode. The check restores 0800 to zero before running the program again. All of these changes pass through real CPU accesses. The video chapter owns the addresses, mode rules, and colour interpretation.

Drawing at high resolution

Type TEXT, then NEW, and enter this program one line at a time. RUN draws five narrow bands: green, violet, white, orange, and blue. The gap between white and orange is a sixth, black band against the black background.

10 HGR
20 FOR C=1 TO 6
30 HCOLOR=C
40 FOR Y=C*20 TO C*20+7
50 HPLOT 14,Y TO 265,Y
60 NEXT Y
70 NEXT C
80 PRINT "HIGH RESOLUTION"
90 END

HGR clears the first high-resolution buffer and selects mixed mode. HPLOT draws a dot or a line between coordinates; here it draws horizontal lines. HCOLOR uses a different numbering from low-resolution COLOR: 0 and 4 are black, 1 green, 2 violet, 3 and 7 white, 5 orange, and 6 blue. The two groups differ in the byte's phase bit. The video chapter explains how dots become colour and the renderer's approximation.

Now type HGR2, then HCOLOR=3, then HPLOT 0,191 TO 279,191. These clear the second high-resolution buffer, select full-screen graphics, and draw a white line at its bottom edge. Full-screen graphics hides the prompt; the keyboard still accepts commands. POKE 49235,0 selects mixed mode and hides those last 32 graphics lines. POKE 49236,0 switches back to page one, revealing the earlier bands. Neither switch clears a buffer or redirects subsequent HPLOT drawing: Applesoft's drawing-page choice is separate from the display switch. TEXT restores the text display, and RUN draws the first program again.

Both graphics buffers occupy main RAM. This short program stays below 2000; larger programs and variables need a memory plan to avoid overwriting graphics or being overwritten by them. The graphics acceptance test runs the exact published program through Applesoft, checks independently specified byte patterns and every displayed colour pair, and exercises page and mode changes without modifying the firmware.

Booting DOS 3.3

The Disk II controller leaves much of the work to software. Its small slot ROM finds and reads the first disk sectors; code loaded from those sectors loads DOS; DOS runs the disk's HELLO program. In this machine the same 6502 executes every stage. No host routine intercepts a ROM call or supplies a DOS command.

Select the ROM container and System Master, then Run. Wait for DOS VERSION 3.3 and the System Master banner. HELLO detects the Language Card and loads Integer BASIC into it before returning to the Applesoft ] prompt. This takes substantially longer than a ROM-only boot; the browser yields between bounded instruction batches so Pause remains available.

Enter CATALOG. DOS shows DISK VOLUME 254 and entries including *A 006 HELLO and *A 009 COLOR DEMOSOFT. A full screen pauses the catalogue: press Enter to continue until ] returns. Then enter these commands separately:

NEW
LOAD HELLO
LIST

LIST displays the program from disk, including the Language Card detection and BLOAD INTBASIC,A$D000 command. Next use NEW, enter the earlier two-line arithmetic program, and try LIST and RUN again. DOS and Applesoft work together; RUN still prints 5. SAVE TEST must instead report WRITE PROTECTED. This milestone cannot save or export disk changes, and never alters the selected file.

Disk II controller and media profile

The machine fence above owns slot placement. A focused TypeScript controller owns the switch effects; DOS-order media owns sector encoding. These remain ordinary device implementations while we gather more peripheral examples. The machine description does not require a new general language for rotating media.

Each access first selects its switch, whether the CPU reads or writes:

Slot-6 address Effect
C0E0–C0E7 Four pairs: phase 0 off/on, phase 1 off/on, phase 2 off/on, phase 3 off/on.
C0E8 / C0E9 Motor off / on.
C0EA / C0EB Select drive 1 / drive 2. Only drive 1 exists in this profile.
C0EC / C0ED Q6 low / high.
C0EE / C0EF Q7 low / high.

Q7 selects read or write operation; Q6 selects shifting or loading/sensing:

Q7 Q6 Modeled result
0 0 Reading C0EC consumes one disk byte. Other even-address reads sample the current latch.
0 1 Even-address reads return 80: the medium is write protected.
1 0 The latch is retained; no bytes are written to the medium.
1 1 CPU writes load the latch, but the protected medium still cannot change.

Odd addresses do not drive the data bus; this model returns 00. In particular, DOS senses protection by reading C0ED and then C0EE: the first access selects Q6 high, and the second supplies bit 7. Reporting protection only on C0ED would miss DOS's actual test. This follows Sather's controller explanation and RWTS listing in Understanding the Apple II, rather than the reference emulator's writable indication.

The controller stores the selected drive (initially 1), motor/Q6/Q7 booleans (all initially false), phase (0), half-track position (0), byte-stream position (0), and data latch (00). Bootstrap and media start absent. CPU reset preserves all of these. Snapshot restoration validates the fields, owns the bootstrap and disk data, and resumes at the same byte without executing a guest access. inspect() reveals control state without copying the medium or advancing it. Insertion and eject reset the byte position and latch, preserving head and controls.

The selected image has 35 tracks of sixteen 256-byte sectors in DOS file order. The encoder builds address fields containing volume 254, track, physical sector, and XOR checksum using four-and-four encoding. Physical sectors 0–15 draw from file sectors 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15. Each data field separates the high six bits of every byte from its reversed low-bit pair. Three groups of low pairs pack into 86 auxiliary values; those precede the 256 high-bit values. An XOR chain and the 64-entry disk-byte translation table produce 343 encoded bytes, including the checksum. Address and data prologues, epilogues, and fixed FF gaps make a circular 6,162-byte track stream.

This is deliberately an access-driven byte model. With motor on, drive 1 selected, and media present, C0EC reads advance that stream. Pausing or inspecting never advances it. Reading without those conditions returns 00 without consuming data; absent drive 2 cannot borrow drive 1's stream or move its head. Selecting a drive clears the data latch. Motor-off is immediate, without the real drive's delay or the reference's browser-frame countdown.

Head movement follows the reference's simplified phase sequence: energizing an adjacent phase moves one half track; the phase-difference table resolves opposite phases. Phase-off accesses do not move the head. The range is clamped to tracks 0–34; odd half tracks read the lower whole track. This is not a model of simultaneous phase currents, head settling, rotational speed, bit timing, the sequencer, or flux transitions. Custom formats, copy protection, other disk sizes, disk writes, and drive 2 are outside this profile. Successful DOS reads establish the stated software checkpoint, not those stronger hardware claims.

Headless acceptance

Synthetic tests exercise the generated map using independently supplied ROM code: real reset-vector reads, keyboard polling and acknowledgement through CPU instructions, recorded bus accesses, ROM immutability, reset preservation, and snapshot continuation. The Language Card checks exercise bank isolation, shared upper RAM, protection, writes behind ROM, restoration between enable reads, and RAM-selected reset vectors. A short 6502 program probes and copies banks while executing from main RAM. The synthetic definition uses its own declared digest; it cannot impersonate this firmware binding.

The external-ROM test boots the selected firmware without the reference emulator's devices or routine traps. It checks the APPLE ][ banner and Applesoft prompt, enters a stored two-line program out of order, checks ordered LIST output, and executes RUN to print 5. It also checks a line-editing backspace, Control-C from a loop, and independent continuation from a hardware snapshot. A CPU copy loop then reads all 12 KiB of firmware into RAM behind ROM, selects the protected RAM copy, and returns to Applesoft there. PRINT 6*7 must still print 42; a subsequent reset must fetch the copied reset vector. This exercises real code in banked RAM, beyond latch inspection. Characters pass through the generated keyboard; output is decoded from RAM, not intercepted at a ROM routine.

APPLE2_ROM=/path/to/apple2p.rom node --test dist/tests/machines/6502/apple2-rom.test.js

Set the same variable for npm test to include the real-ROM acceptance run. Without it, that test is explicitly skipped; synthetic machine, device, and media-verification checks still run. No test downloads firmware.

The disk acceptance test uses the same host media verification and keyboard queue as the browser. It cold-boots DOS, restores and compares instruction records while the bootstrap is reading, finishes CATALOG pagination, clears BASIC before LOAD/LIST HELLO, and runs the arithmetic program under DOS. SAVE must report protection and leave every media byte unchanged. Reset, eject, and fresh power-on check media retention separately.

APPLE2_ROM=/path/to/apple2p.rom APPLE2_DISK=/path/to/dos33-master.dsk npm test

This test requires the 20 KiB ROM container and is explicitly skipped when either variable is absent. The encoding vectors independently specify sector order, address fields, low-pair packing, shortened auxiliary groups, and checksums. Controller checks cover switch direction, circular reads, head bounds, sense/write modes, missing media/drive, snapshot continuation, and invalid input without depending on firmware.

For these checks, text rows use the independently transcribed page-one base addresses 0400, 0480, …, 0780; 0428, 04A8, …, 07A8; then 0450, 04D0, …, 07D0. Each row contains forty characters. Low six bits select the original uppercase character set; inverse/flashing attributes are retained in RAM but not rendered by this test. The independent video tests check both pages and all attributes. The browser-session tests use the production file verifier, keyboard queue, scheduler, and frame decoder with the real ROM, including paste, editing, stored programs, pause/resume, and Control-C. Manual browser checks also cover file selection, keyboard focus, reset/power-on, failed replacement, page hiding, and wide/narrow layouts.