// Commander X16 Emulator — disassembly-accuracy core ("code map"). // See code_map.h for the rationale. This file is deliberately dependency-light // (no SDL / glue.h) so it stays warning-clean under /W3 /WX and is easy to unit // test in isolation: it leans on just disasm(), dbg_info, and the two core // symbols declared directly below. #include "code_map.h" #include "disasm.h" #include "dbg_info.h" #include "cpu/registers.h" // FLAG_* bits + struct regs layout #include #include #include // snprintf, for rendering a cut-short line as data bytes // --- Core symbols, declared directly to avoid pulling in memory.h/glue.h --- // The global CPU register file (defined in fake6502.c) and the non-intrusive // memory read used by disasm(). Signatures match memory.h / glue.h exactly. extern struct regs regs; extern bool is_gen2; uint8_t real_read6502(uint16_t address, uint8_t bank, bool debugOn, int16_t x16Bank); #define CM_X16_CURRENT_BANK ((int16_t)-1) #define CM_READ(a, b, xb) real_read6502((uint16_t)(a), (uint8_t)(b), true, (int16_t)(xb)) // Longest instruction we ever have to back up over when self-syncing (the // 65C816 tops out at 4 bytes). #define CM_MAX_INSN_LEN 4 // --------------------------------------------------------------------------- // Coverage + flag storage, keyed by the bank context backing the address. // --------------------------------------------------------------------------- // Each context is a 64K bit coverage map (1 bit per address = 8 KB) plus a // per-address effective-status byte. Contexts are allocated lazily and capped; // the least-recently-used one is recycled once the cap is reached. // // The key is deliberately NOT the full live bank state. Only part of the // address space is banked, so keying every address by (rambank, rombank) would // file the same unbanked low-memory code under a fresh context for every bank // combination the program happens to be in. With 32 ROM banks and 256 RAM // banks that exhausts the context cache during ROM boot alone (measured: the // cap is hit before BASIC is ready), and worse, coverage recorded at $0801 // under one ROM bank could no longer be found once the program switched banks // -- so the disassembler lost the very ground truth it exists to collect. // // Instead each address is keyed by what actually backs it, mirroring the // decode in real_read6502(): // - gen2 with a non-zero program bank: the whole 64K is flat bank RAM and // the window registers select nothing, so only the program bank keys it; // - otherwise the program bank selects nothing (read6502() forces it to 0 on // gen1, even with -c816), and the key is the RAM bank for $A000-$BFFF, the // ROM bank for $C000-$FFFF, and nothing at all below $A000. // Anchors are never deleted when the memory under them changes -- there is no // write hook, and an overlay LOAD or a decompressor replaces code wholesale -- // so each anchor also stores the opcode byte that was executing. An anchor is // only believed while that byte is still there. Without this, running a second // program over the first leaves stale "instruction starts" mid-instruction in // the new code, and they outrank the .dbg info that would have been right. #define CM_ADDR_SPACE 0x10000 #define CM_COVER_BYTES (CM_ADDR_SPACE / 8) // 8 KB: one bit per address // Contexts cost ~136 KB each. Keying by backing memory (above) means a real ROM // boot settles at four, so a cap of 16 leaves ample headroom at ~2.2 MB total. #define CM_MAX_CONTEXTS 16 typedef struct { bool used; uint8_t pbank; uint8_t rambank; uint8_t rombank; uint64_t lru; uint8_t cover[CM_COVER_BYTES]; uint8_t status[CM_ADDR_SPACE]; // valid only where the cover bit is set uint8_t opcode[CM_ADDR_SPACE]; // ditto: the byte that was executing } cm_context_t; static cm_context_t *g_ctx[CM_MAX_CONTEXTS]; static uint64_t g_lru_clock; static cm_context_t *g_last_ctx; // fast-path cache for the hot record loop // Which banked window a given address reads from, for the disasm/real_read6502 // x16Bank argument. Low memory ($0000-$9FFF) is not banked (the CPU program // bank handles gen2), so it follows the current bank. static int16_t cm_x16bank_for(uint16_t addr, uint8_t rambank, uint8_t rombank) { if (addr >= 0xC000) return (int16_t)rombank; if (addr >= 0xA000) return (int16_t)rambank; return CM_X16_CURRENT_BANK; } // Reduce the live bank state to the part that actually selects the memory // behind `addr`. See the note above. static void cm_key_for(uint16_t addr, uint8_t pbank, uint8_t rambank, uint8_t rombank, uint8_t *key_pb, uint8_t *key_ram, uint8_t *key_rom) { if (is_gen2 && pbank != 0) { *key_pb = pbank; *key_ram = 0; *key_rom = 0; return; } *key_pb = 0; *key_ram = (addr >= 0xA000 && addr < 0xC000) ? rambank : 0; *key_rom = (addr >= 0xC000) ? rombank : 0; } static cm_context_t * cm_ctx_find(uint8_t pbank, uint8_t rambank, uint8_t rombank) { for (int i = 0; i < CM_MAX_CONTEXTS; i++) { cm_context_t *c = g_ctx[i]; if (c && c->used && c->pbank == pbank && c->rambank == rambank && c->rombank == rombank) return c; } return NULL; } static cm_context_t * cm_ctx_get_or_create(uint8_t pbank, uint8_t rambank, uint8_t rombank) { cm_context_t *c = cm_ctx_find(pbank, rambank, rombank); if (c) return c; // Prefer a free slot; otherwise evict the least-recently-used context. int slot = -1; for (int i = 0; i < CM_MAX_CONTEXTS; i++) { if (!g_ctx[i] || !g_ctx[i]->used) { slot = i; break; } if (slot < 0 || g_ctx[i]->lru < g_ctx[slot]->lru) slot = i; } if (slot < 0) slot = 0; if (!g_ctx[slot]) { g_ctx[slot] = (cm_context_t *)calloc(1, sizeof(cm_context_t)); if (!g_ctx[slot]) return NULL; } else { // Recycling: only the coverage bits must be cleared — status bytes are // never read unless their cover bit is set. The evicted context may be // the one g_last_ctx points at, so drop that cache too rather than let // it name a context that now holds different banks. memset(g_ctx[slot]->cover, 0, sizeof(g_ctx[slot]->cover)); if (g_last_ctx == g_ctx[slot]) g_last_ctx = NULL; } g_ctx[slot]->used = true; g_ctx[slot]->pbank = pbank; g_ctx[slot]->rambank = rambank; g_ctx[slot]->rombank = rombank; g_ctx[slot]->lru = ++g_lru_clock; return g_ctx[slot]; } static bool cm_bit(const cm_context_t *c, uint16_t addr) { return c && (c->cover[addr >> 3] & (uint8_t)(1u << (addr & 7))) != 0; } // True if `addr` is a recorded instruction start AND the opcode byte recorded // there still matches memory. // // Anchors are only ever added; nothing removes them when memory changes, and on // this machine memory under code changes all the time -- an overlay LOAD, a // second program loaded over the first, a decompressor unpacking over its own // loader, self-modifying code. A stale anchor is worse than no anchor, because // live-execution evidence outranks the .dbg spans that would have been right, // so it can confidently place an instruction boundary in the middle of a real // instruction. // // Comparing the recorded opcode byte against memory catches most of those for // the price of one byte per address, and it keeps the DETECTION off the hot // path -- staleness is noticed when an anchor is *used*, not when memory is // written, so no write hook is needed. (Recording still pays one read and one // store per executed instruction to capture the byte.) It is a heuristic, not a // proof: replacement code that repeats the same opcode byte at the same address // slips through. See the note in code_map.h and the KNOWN LIMITATION checks in // tests/test_code_map.c. static bool cm_anchor_ok(const cm_context_t *c, uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank) { if (!cm_bit(c, addr)) return false; return c->opcode[addr] == (uint8_t)CM_READ(addr, bank, cm_x16bank_for(addr, rambank, rombank)); } // --------------------------------------------------------------------------- // Recording // --------------------------------------------------------------------------- void code_map_record(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank, uint8_t status) { uint8_t k_pb, k_ram, k_rom; cm_key_for(pc, pbank, rambank, rombank, &k_pb, &k_ram, &k_rom); // Fast path: this is called for every executed instruction, so avoid the // context scan -- and the LRU bookkeeping -- when the bank context has not // changed since last time, which is overwhelmingly the common case. cm_context_t *c = g_last_ctx; if (!c || !c->used || c->pbank != k_pb || c->rambank != k_ram || c->rombank != k_rom) { c = cm_ctx_get_or_create(k_pb, k_ram, k_rom); if (!c) return; c->lru = ++g_lru_clock; g_last_ctx = c; } c->cover[pc >> 3] |= (uint8_t)(1u << (pc & 7)); c->status[pc] = status; c->opcode[pc] = (uint8_t)CM_READ(pc, pbank, cm_x16bank_for(pc, rambank, rombank)); } // Locate the context holding coverage for `addr` under the given live banks. static cm_context_t * cm_ctx_for_addr(uint16_t addr, uint8_t pbank, uint8_t rambank, uint8_t rombank) { uint8_t k_pb, k_ram, k_rom; cm_key_for(addr, pbank, rambank, rombank, &k_pb, &k_ram, &k_rom); return cm_ctx_find(k_pb, k_ram, k_rom); } bool code_map_is_recorded_start(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank) { return cm_anchor_ok(cm_ctx_for_addr(pc, pbank, rambank, rombank), pc, pbank, rambank, rombank); } uint8_t code_map_recorded_status(uint16_t pc, uint8_t pbank, uint8_t rambank, uint8_t rombank, uint8_t fallback) { cm_context_t *c = cm_ctx_for_addr(pc, pbank, rambank, rombank); if (cm_anchor_ok(c, pc, pbank, rambank, rombank)) return c->status[pc]; return fallback; } void code_map_reset(void) { for (int i = 0; i < CM_MAX_CONTEXTS; i++) { if (g_ctx[i]) { g_ctx[i]->used = false; memset(g_ctx[i]->cover, 0, sizeof(g_ctx[i]->cover)); } } g_lru_clock = 0; g_last_ctx = NULL; } // --------------------------------------------------------------------------- // Decoding helpers // --------------------------------------------------------------------------- static int cm_decode(uint16_t addr, uint8_t bank, int16_t x16bank, uint8_t status, char *line, unsigned line_sz, int32_t *eff) { int32_t e = -1; int sz = disasm(addr, bank, NULL, line, line_sz, x16bank, status, &e); if (sz < 1) sz = 1; if (eff) *eff = e; return sz; } // Predict the implied status of the NEXT instruction from the current opcode. // Only used to fill gaps where live coverage is missing; recorded status always // takes precedence over this estimate. // // Just five instructions change the register widths -- REP, SEP, XCE, PLP and // RTI. REP and SEP apply their own status bits exactly, since the bits are in // the operand; the WIDTHS they imply are still only as good as the E estimate // below, because emulation mode overrides them. // // XCE is exact only when BOTH of its inputs are right, and neither is // guaranteed. It swaps the carry and emulation flags, so it needs the incoming // carry -- which is carried through the instructions handled in this switch // (CLC, SEC, and whatever REP/SEP/XCE themselves do to it), but NOT through any // data-dependent change: ADC, SBC, the compares, the shifts and rotates all // move the carry without this estimate noticing. It also needs the incoming E, // which anchors do not record at all. // // PLP and RTI restore a status byte from the stack, so this deliberately leaves // the estimate alone for them rather than inventing a value. // // HOW WELL EACH GAP IS BOUNDED differs, and none is bounded unconditionally: // - a wrong STATUS estimate can be corrected when an accurate, believed // anchor is reached. Not "the next recorded anchor": a same-opcode stale // anchor is still believed and hands back its old status, there may be no // accurate anchor ahead at all, and even an accurate one only fixes its OWN // line -- an anchor on a PLP or RTI gives the status going INTO it, and the // byte it restores is still unmodelled, so the next line can be wrong again; // - a wrong E is weaker again, because anchors do not store E. An anchor // fixes the width of its own line, and propagation folds the stale E back // in (`if (e) status |= INDEX|MEMORY`), which can mis-size the next // unanchored line wherever the stale E and the real one imply different // widths. It is not unrecoverable: an XCE decoded with an accurate carry // writes a correct E (that is exactly what the real instruction does), as // does an explicit CLC/SEC before one -- but nothing guarantees such a // sequence appears. // That fold-in also means "emulation mode is always right" holds for the real // CPU but NOT for this estimate: a diverged E sizes operands as though native // while the machine is really in emulation mode. // Either way every line drawn from the estimate reports recorded = false, so a // consumer is never told a guess is ground truth. All of this is pinned by // tests. static uint8_t cm_propagate(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank, uint8_t status, uint8_t *e_inout) { if (!regs.is65c816) return status; // The operand is read through the window backing the OPERAND's address, // not the opcode's. They differ for an instruction whose opcode is the last // byte of a window: a REP at $BFFF takes its operand from $C000, which is // banked ROM, and reading that through the RAM window yields some other // bank's byte. Getting it wrong here mis-sizes the following instructions // until an accurate anchor is reached -- the exact drift this file exists // to prevent. uint16_t argp = (uint16_t)((addr + 1) & 0xFFFF); int16_t xb_op = cm_x16bank_for(addr, rambank, rombank); int16_t xb_arg = cm_x16bank_for(argp, rambank, rombank); uint8_t opcode = (uint8_t)CM_READ(addr, bank, xb_op); uint8_t e = *e_inout; switch (opcode) { case 0x18: // CLC status &= (uint8_t)~FLAG_CARRY; break; case 0x38: // SEC status |= FLAG_CARRY; break; case 0xC2: // REP #imm — clears the given status bits status &= (uint8_t)~CM_READ(argp, bank, xb_arg); break; case 0xE2: // SEP #imm — sets the given status bits status |= (uint8_t)CM_READ(argp, bank, xb_arg); break; case 0xFB: { // XCE — exchange carry and emulation flags uint8_t carry = (uint8_t)(status & FLAG_CARRY); status = (uint8_t)((status & (uint8_t)~FLAG_CARRY) | (e ? FLAG_CARRY : 0)); e = carry ? 1 : 0; break; } // TODO: PLP (0x28) and RTI (0x40) also change the widths, by restoring a // status byte from the stack. Nothing is done for them because the byte // is not knowable from the instruction stream -- see the header note on // where the widths are still a guess. Two avenues if this is ever worth // closing, both of which recover a real value rather than guessing: // - PLP is almost always the tail of a PHP/PLP save-restore pair. Walk // back to the matching PHP within the same routine; if the status // there is known, so is the status the PLP restores. // - RTI restores what the interrupt pushed, which is the status at the // moment the interrupt was taken. The emulator sees every interrupt, // so that value could simply be recorded when it happens. // Both need a notion of "known vs unknown" per status bit so a failed // attempt reports uncertainty instead of substituting another guess. default: break; } // In emulation mode the 65C816 forces 8-bit memory/index widths. if (e) status |= (uint8_t)(FLAG_INDEX_WIDTH | FLAG_MEMORY_WIDTH); *e_inout = e; return status; } // Fill one line at `addr`, decoded with an explicit status. // // The size the decode produces is clamped twice, and both clamps exist because // a decode is the weakest evidence in this file: // // * `max_size`, when positive, is a boundary the caller has already committed // to -- the address the next line starts at. The backward walk resolves // that boundary from evidence the fill does not have (and its last-resort // fallback does not decode at all, it just backs off a byte), so a decode // that disagrees has to give way rather than emit a line overlapping the // next one. // * a recorded anchor inside the instruction is live-execution evidence that // a real instruction starts there, which outranks a width ESTIMATE; // truncate onto it rather than stepping over the very ground truth this // file exists to collect. This applies only when the line being decoded is // itself a guess. When the line is recorded too, its width came from the // status captured as it executed and is evidence of the same rank, so // the two anchors are simply two real, overlapping instruction starts -- // the `.byte $2C` skip idiom -- and the one the caller asked for wins. // Clipping it there would discard a whole known instruction to display a // different execution path than the one requested. // // Either clamp can leave the line shorter than the decode wanted. Such a line // no longer describes a whole instruction, so it is not reported as one: it // comes back with `recorded == false`, no effective address, and `text` // rewritten as the raw data bytes it actually owns, so `size`, `bytes` and // `text` always agree. Covering the leftover bytes is the caller's job; see the // tiling loop in code_map_disasm_window(). static int cm_fill(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank, uint8_t st, bool recorded, int max_size, code_map_line_t *ln) { int16_t xb = cm_x16bank_for(addr, rambank, rombank); int32_t eff; int sz = cm_decode(addr, bank, xb, st, ln->text, (unsigned)sizeof(ln->text), &eff); int decoded = sz; if (max_size > 0 && sz > max_size) sz = max_size; // Only a guessed width gives way to an interior anchor. A recorded line's // width came from the status captured as it executed, so it is evidence of // the same rank -- two overlapping real instruction starts, not a bad guess // straddling a good one. // // The exemption is re-derived here rather than taken from the `recorded` // argument, because it is only sound while `st` really is the status the // anchor recorded: a caller that passed `recorded` with some other status // would be decoding at a guessed width and would then be allowed to step // over the very ground truth this clamp protects. The one caller does // satisfy that today; checking it locally means a second one cannot quietly // break it. (Defensive: no current path violates it, so this is not // exercised by a test.) cm_context_t *self = cm_ctx_for_addr(addr, bank, rambank, rombank); bool truth = cm_anchor_ok(self, addr, bank, rambank, rombank) && self->status[addr] == st; if (!truth) { for (int i = 1; i < sz; i++) { uint16_t p = (uint16_t)((addr + i) & 0xFFFF); if (cm_anchor_ok(cm_ctx_for_addr(p, bank, rambank, rombank), p, bank, rambank, rombank)) { sz = i; break; } } } // A line the decode did not get to finish is not an instruction, and must // not be dressed as one: `text` would describe more bytes than `size` // covers, and `eff` was computed from bytes the line does not own -- for a // clamped line those bytes are the NEXT instruction's opcode. Render the // bytes it does own as data instead, so size, bytes and text agree and a // consumer (the panel, or DAP's instruction/instructionBytes pair) cannot // be handed a self-contradictory row. // // This is the honest rendering of a genuine ambiguity rather than a // failure: the `.byte $2C` skip idiom really does put two overlapping // instruction starts in memory, both real and both executed, and no linear // non-overlapping tiling can show both whole. The policy is that the start // the caller selected wins -- forward from the outer one shows the outer // instruction, forward from the inner one shows the inner -- and only a // guessed decode, or one crossing a boundary the window already committed // to, is reduced to the bytes it owns on that path. if (sz < decoded) { recorded = false; eff = -1; int used = 0; for (int b = 0; b < sz && b < 4; b++) { uint16_t p = (uint16_t)((addr + b) & 0xFFFF); int w = snprintf(ln->text + used, sizeof(ln->text) - (size_t)used, "%s$%02x", b == 0 ? ".byte " : ",", (unsigned)CM_READ(p, bank, cm_x16bank_for(p, rambank, rombank))); if (w < 0 || (size_t)(used + w) >= sizeof(ln->text)) break; used += w; } } ln->addr = addr; ln->size = (uint8_t)sz; ln->status = st; ln->eff_addr = eff; ln->recorded = recorded; ln->kind = (uint8_t)(sz < decoded ? CM_LINE_DATA : CM_LINE_INSTRUCTION); // Whether the ADDRESS was executed is separate from whether this row could // be shown as a whole instruction: a boundary can force a recorded start to // be emitted as data, and a consumer still wants to know it is real code. ln->start_recorded = cm_anchor_ok(self, addr, bank, rambank, rombank); memset(ln->bytes, 0, sizeof(ln->bytes)); // Each byte is read through the window that backs its own address, so an // instruction straddling $BFFF/$C000 shows the ROM bank's bytes for the part // that lives there rather than the RAM bank's. for (int b = 0; b < sz && b < 4; b++) { uint16_t p = (uint16_t)((addr + b) & 0xFFFF); ln->bytes[b] = (uint8_t)CM_READ(p, bank, cm_x16bank_for(p, rambank, rombank)); } return sz; } // Emit one line at `addr` and advance the running status estimate. Recorded // status wins; otherwise *run_status is used. `max_size` is passed through to // cm_fill. Returns the number of bytes the emitted row covers (always >= 1), // which is the instruction's size unless a clamp cut the row short. static int cm_emit(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank, uint8_t *run_status, uint8_t *run_e, int max_size, code_map_line_t *ln) { cm_context_t *c = cm_ctx_for_addr(addr, bank, rambank, rombank); bool recorded = cm_anchor_ok(c, addr, bank, rambank, rombank); uint8_t st = recorded ? c->status[addr] : *run_status; int sz = cm_fill(addr, bank, rambank, rombank, st, recorded, max_size, ln); *run_status = cm_propagate(addr, bank, rambank, rombank, st, run_e); return sz; } // --------------------------------------------------------------------------- // Anchored alignment // --------------------------------------------------------------------------- // The decision the backward walk made about one instruction: where it starts, // how long it is, and the status it was sized with. Phase B seeds itself from // this rather than re-deriving the boundary from a propagated estimate, which // is what used to make the walk step over `center`. It does NOT render the line // verbatim -- cm_emit re-resolves the evidence at each address, and on the // fallback path can reach a different answer; see the note in phase B. typedef struct { uint16_t addr; uint8_t status; uint8_t size; bool recorded; } cm_anchor_t; static bool cm_prev_anchor(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank, cm_anchor_t *out) { cm_context_t *ac = cm_ctx_for_addr(addr, bank, rambank, rombank); uint8_t anchor_status = cm_anchor_ok(ac, addr, bank, rambank, rombank) ? ac->status[addr] : regs.status; cm_anchor_t best_recorded = { 0, 0, 0, false }; cm_anchor_t best_span = { 0, 0, 0, false }; cm_anchor_t best_sync = { 0, 0, 0, false }; bool has_recorded = false, has_span = false, has_sync = false; for (int backoff = 1; backoff <= CM_MAX_INSN_LEN; backoff++) { uint16_t p = (uint16_t)((addr - backoff) & 0xFFFF); int16_t xb = cm_x16bank_for(p, rambank, rombank); // A candidate may sit in a different window than the anchor (a window // boundary can fall mid-instruction), so resolve its context itself. cm_context_t *c = cm_ctx_for_addr(p, bank, rambank, rombank); bool recorded = cm_anchor_ok(c, p, bank, rambank, rombank); uint8_t st = recorded ? c->status[p] : anchor_status; char scratch[48]; int32_t eff; int sz = cm_decode(p, bank, xb, st, scratch, (unsigned)sizeof(scratch), &eff); // A candidate is only valid if it decodes to land exactly on `addr`. if (sz != backoff) continue; cm_anchor_t cand = { p, st, (uint8_t)sz, recorded }; if (!has_recorded && recorded) { best_recorded = cand; has_recorded = true; } // The .dbg address space is a flat 24-bit one, so the program bank has // to be folded in -- otherwise bank-K code never matches its own spans, // and a bank-0 span at the same offset matches when it should not. if (!has_span && dbg_info_is_span_start(((dbg_addr_t)bank << 16) | p)) { best_span = cand; has_span = true; } if (!has_sync) { best_sync = cand; has_sync = true; } } if (has_recorded) { *out = best_recorded; return true; } // (1) live coverage if (has_span) { *out = best_span; return true; } // (2) cc65 .dbg spans if (has_sync) { *out = best_sync; return true; } // (3) self-sync // Nothing aligned cleanly. Back up a single byte so callers still make // progress, and say the size that implies. out->addr = (uint16_t)((addr - 1) & 0xFFFF); out->status = anchor_status; out->size = 1; out->recorded = false; return true; } uint16_t code_map_prev_instruction(uint16_t addr, uint8_t bank, uint8_t rambank, uint8_t rombank) { cm_anchor_t an; if (!cm_prev_anchor(addr, bank, rambank, rombank, &an)) return (uint16_t)((addr - 1) & 0xFFFF); return an.addr; } int code_map_disasm_window(uint16_t center, uint8_t bank, uint8_t rambank, uint8_t rombank, int lines_before, int lines_after, code_map_line_t *out, int max_out, int *out_center_index) { if (out_center_index) *out_center_index = -1; if (!out || max_out <= 0) return 0; if (lines_before < 0) lines_before = 0; if (lines_after < 1) lines_after = 1; // always include the center line itself // Phase A — walk backward, keeping the decision made about each instruction // rather than just its address. Re-deriving these on a forward pass would // use a different (propagated) status estimate, and where the two disagree // the walk drifts: it steps over `center`, loses the current-line marker and // renders every following line at the wrong offset. cm_anchor_t before[256]; int nbefore = lines_before > 255 ? 255 : lines_before; int collected = 0; uint16_t a = center; for (int i = 0; i < nbefore; i++) { cm_anchor_t an; if (!cm_prev_anchor(a, bank, rambank, rombank, &an)) break; if (an.addr == a) break; before[collected++] = an; a = an.addr; } // Phase B — render the preceding instructions, tiling the range the backward // walk resolved: each line must start exactly where the previous one ended, // or the caller cannot draw the result. // // The walk fixes where each instruction ENDS (the next line's start), but // the fill can still land short of it or, left unchecked, past it: // * the walk's last-resort fallback backs off one byte without decoding, // so the byte there can decode wider than the one byte allowed for; // * the fill truncates onto a recorded start found inside the // instruction, which is right but leaves the remaining bytes uncovered. // Capping each fill at the distance to the boundary kills the overlap, and // looping until the boundary is reached exactly kills the gap. // // run_status is seeded from the walk's decision, but cm_emit re-resolves // the evidence at each address rather than taking before[i] verbatim, so // the first line is not guaranteed to match what the walk decided. It can // differ on the fallback path: the walk rejects a candidate whose decode // does not land on the anchor and stores the anchor's status, while cm_emit // looks up that address and may find a recorded anchor of its own. Where // they differ, cm_emit's answer is the better one -- it is live-execution // evidence recorded at that exact address -- and it cannot drift, because // the fallback has one byte of room and phase C reseeds independently. // // Because one resolved instruction can now need more than one line, this // resolves them NEAREST-THE-CENTER FIRST and places them backwards from the // end of the reserved region. Filling forwards would spend the buffer on // the furthest context and push `center` -- the address the caller asked to // be centered on -- off the end of a full buffer, which is the one row every // consumer most needs. One slot is always held back for it. code_map_line_t tile[CM_MAX_INSN_LEN]; const int b_cap = max_out > 1 ? max_out - 1 : 0; int write = b_cap; for (int i = 0; i < collected && write > 0; i++) { uint16_t limit = (i > 0) ? before[i - 1].addr : center; uint16_t a = before[i].addr; uint8_t run_status = before[i].status; uint8_t run_e = regs.e; int k = 0; bool closed = false; // Terminates: every emit covers at least one byte and never more than // the bytes left, so `a` closes on `limit` monotonically. `room` starts // at before[i].size, which the walk bounds by CM_MAX_INSN_LEN. do { int room = (int)(uint16_t)((limit - a) & 0xFFFF); int sz = cm_emit(a, bank, rambank, rombank, &run_status, &run_e, room, &tile[k]); k++; a = (uint16_t)((a + sz) & 0xFFFF); if (sz >= room) { closed = true; break; } } while (k < CM_MAX_INSN_LEN); // Commit only a group that reached its boundary exactly; one that did // not is not a tiling of anything. // // A group too large for the space left is trimmed to its SUFFIX -- the // lines nearest the center -- rather than dropped whole. Every line in // the group is contiguous and the last one ends on the boundary, so any // suffix still tiles; it just starts the window later. (A prefix would // not: it would end short of the boundary and reintroduce the gap this // loop exists to remove.) Dropping the group instead would throw away // context the caller has room for. if (!closed) break; int from = 0; if (k > write) { from = k - write; k = write; } write -= k; memcpy(&out[write], &tile[from], (size_t)k * sizeof(*tile)); } int n = b_cap - write; if (write > 0 && n > 0) memmove(&out[0], &out[write], (size_t)n * sizeof(*out)); // Phase C — forward from `center` itself, so the center line is always // present and always starts where the caller asked. The slot reserved above // guarantees n < max_out here whenever the caller gave any capacity at all. if (n < max_out && out_center_index) *out_center_index = n; uint16_t addr = center; uint8_t run_status = code_map_recorded_status(center, bank, rambank, rombank, regs.status); uint8_t run_e = regs.e; for (int i = 0; i < lines_after && n < max_out; i++) { int sz = cm_emit(addr, bank, rambank, rombank, &run_status, &run_e, 0, &out[n]); n++; addr = (uint16_t)((addr + sz) & 0xFFFF); } return n; } int code_map_disasm_forward(uint16_t start, uint8_t bank, uint8_t rambank, uint8_t rombank, int count, code_map_line_t *out, int max_out, uint16_t *next_addr) { if (!out || max_out <= 0) { if (next_addr) *next_addr = start; return 0; } if (count > max_out) count = max_out; if (count < 0) count = 0; uint16_t addr = start; uint8_t run_status = code_map_recorded_status(start, bank, rambank, rombank, regs.status); uint8_t run_e = regs.e; int n = 0; while (n < count) { int sz = cm_emit(addr, bank, rambank, rombank, &run_status, &run_e, 0, &out[n]); n++; addr = (uint16_t)((addr + sz) & 0xFFFF); } if (next_addr) *next_addr = addr; return n; }