All files / src paging.js

100% Statements 92/92
97.01% Branches 65/67
100% Functions 19/19
100% Lines 74/74

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176                    137x 137x 137x 137x   137x 137x 137x 137x 137x   137x 137x     137x 137x   137x 137x 137x   137x 137x 137x       12132x         161x 161x 3x     10578x   403x       27836x         611x 604x 604x 604x                 108x 108x 108x         108x 29x 29x         13978707x 13978707x   13881819x   2x   5x   6x   96875x               13968172x 13968172x 13968172x                 24x 24x 24x 5x 5x   19x 19x         10511x 10511x 10511x 73x               6x 4x       51x 212x 51x       126x 4x                   164x 164x 164x 19x 18x 164x 164x         521x 164x 146x    
import { NumRomBanks, RomBankBytes } from "./6502.js";
import { byteChar } from "./hex.js";
 
/**
 * A view of memory the debugger can show instead of what the CPU has paged in: one sideways bank at &8000 to
 * &BFFF, or on a Master ANDY at &8000 to &8FFF, or main or shadow RAM at &3000 to &7FFF. Everywhere else it
 * reads as paged. A view is named by the one character that qualifies an address in Go to (8000`4, 4:8000).
 * @typedef {string} PagingView
 */
 
export const PagedView = "";
export const AndyView = "n";
export const ShadowView = "s";
export const MainView = "m";
 
export const SidewaysStart = 0x8000;
export const SidewaysEnd = 0xc000;
const AndyEnd = 0x9000;
const ShadowStart = 0x3000;
const ShadowEnd = 0x8000;
// Where romSelect and writeAcccon (6502.js) map ANDY and LYNNE, the shadow RAM, in ramRomOs.
const AndyOffset = 0;
const LynneOffset = 0x8000;
 
// As the CPU's memStat marks a page: a write to ROM changes nothing, and one to neither reaches a device.
export const RamStat = 1;
export const RomStat = 2;
 
const CopyrightPointerOffset = 7;
const TitleOffset = 9;
const Copyright = "(C)";
 
const ViewChar = "[0-9a-fA-FnNsSmM]";
const SuffixForm = new RegExp(`^(.+)\`(${ViewChar})$`);
const PrefixForm = new RegExp(`^(${ViewChar}):(.+)$`);
 
/** The banks ROMSEL can page in: all sixteen, or on a four-socket B the four its two bits reach. */
export function sidewaysBanks({ romSockets }) {
    return Array.from({ length: romSockets }, (_, i) => NumRomBanks - romSockets + i);
}
 
/** Throws unless `bank`, if given, is a sideways bank. */
export function requireBank(bank, model) {
    const banks = sidewaysBanks(model);
    if (bank !== undefined && !banks.includes(bank))
        throw new Error(`Bank ${bank} is not ${banks[0]} to ${banks[banks.length - 1]}`);
}
 
export const bankView = (bank) => bank.toString(16);
 
export const bankName = (bank) => bankView(bank).toUpperCase();
 
/** @returns {number | undefined} the sideways bank `view` shows, if it shows one */
export function viewedBank(view) {
    return /^[0-9a-f]$/.test(view) ? parseInt(view, 16) : undefined;
}
 
/** Every view `model` has, the paged one first. */
export function viewsFor(model) {
    if (model.isAtom) return [PagedView];
    const views = [PagedView, ...sidewaysBanks(model).map(bankView)];
    if (model.isMaster) views.push(AndyView, MainView, ShadowView);
    return views;
}
 
/**
 * Splits a Go to text into the address it names and the view it qualifies it with, if any: 8000`4 or 4:8000 for
 * bank 4, and n, s and m for ANDY, shadow and main RAM. Undefined when it names a view `model` doesn't have.
 * @returns {{address: string, view: PagingView | undefined} | undefined}
 */
export function parseViewedAddress(text, model) {
    const suffixed = SuffixForm.exec(text);
    const prefixed = PrefixForm.exec(text);
    const [address, qualifier] = suffixed
        ? [suffixed[1], suffixed[2]]
        : prefixed
          ? [prefixed[2], prefixed[1]]
          : [text, undefined];
    if (qualifier === undefined) return { address, view: undefined };
    const view = qualifier.toLowerCase();
    return viewsFor(model).includes(view) && view !== PagedView ? { address: address.trim(), view } : undefined;
}
 
/** Where in `cpu.ramRomOs` `view` reads `address`, or undefined where it reads as paged. */
function viewedOffset(cpu, view, address) {
    const inside = (start, end) => address >= start && address < end;
    switch (view) {
        case PagedView:
            return undefined;
        case AndyView:
            return inside(SidewaysStart, AndyEnd) ? AndyOffset + address : undefined;
        case ShadowView:
            return inside(ShadowStart, ShadowEnd) ? LynneOffset + address : undefined;
        case MainView:
            return inside(ShadowStart, ShadowEnd) ? address : undefined;
        default:
            return inside(SidewaysStart, SidewaysEnd)
                ? cpu.romOffset + viewedBank(view) * RomBankBytes + address - SidewaysStart
                : undefined;
    }
}
 
/** The byte at `address` as `view` shows it, read as peekmem reads, without side effects. */
export function peekViewed(cpu, view, address) {
    address &= 0xffff;
    const offset = viewedOffset(cpu, view, address);
    return offset === undefined ? cpu.peekmem(address) : cpu.ramRomOs[offset];
}
 
/**
 * What a write must reach to change the byte `view` shows at `address`: its memStat (RAM, ROM or a device register)
 * and where it is in `cpu.ramRomOs`, as the CPU pages it now where the view reads as paged.
 * @returns {{stat: number, offset: number}}
 */
export function viewedByte(cpu, view, address) {
    address &= 0xffff;
    const offset = viewedOffset(cpu, view, address);
    if (offset !== undefined) {
        const bank = viewedBank(view);
        return { stat: bank === undefined || cpu.model.swram[bank] ? RamStat : RomStat, offset };
    }
    const page = cpu.memStatOffset + (address >>> 8);
    return { stat: cpu.memStat[page], offset: cpu.memLook[page] + address };
}
 
/** Whether `view` shows something at `address` other than what the CPU would read there now. */
export function viewDiffers(cpu, view, address) {
    address &= 0xffff;
    const offset = viewedOffset(cpu, view, address);
    if (offset === undefined) return false;
    return cpu.memLook[cpu.memStatOffset + (address >>> 8)] + address !== offset;
}
 
/**
 * The view showing the RAM the video reads at screen `address`, &3000 to &7FFF: shadow whenever ACCCON's D bit has
 * the video reading it, since the CPU's paging of it changes with X, E and the program counter.
 */
export function screenView(cpu, address) {
    if (cpu.videoDisplayPage) return ShadowView;
    return viewDiffers(cpu, MainView, address) ? MainView : PagedView;
}
 
function text(peek, from, to) {
    let read = "";
    for (let address = from; address < to; ++address) read += byteChar(peek(address));
    return read;
}
 
function zeroAt(peek, from, to) {
    for (let address = from; address < to; ++address) if (peek(address) === 0) return address;
    return undefined;
}
 
/**
 * The title and version string in the header of the sideways ROM `peek` reads at &8000, unprintable bytes shown as
 * dots, or undefined if there is no ROM there: no copyright string where the header points to one.
 * @param {(address: number) => number} peek
 * @returns {{title: string, version: string} | undefined}
 */
function romHeader(peek) {
    const copyright = SidewaysStart + peek(SidewaysStart + CopyrightPointerOffset);
    const titleStart = SidewaysStart + TitleOffset;
    if (copyright <= titleStart || peek(copyright) !== 0) return undefined;
    if (text(peek, copyright + 1, copyright + 1 + Copyright.length) !== Copyright) return undefined;
    const titleEnd = zeroAt(peek, titleStart, copyright) ?? copyright;
    const version = titleEnd < copyright ? text(peek, titleEnd + 1, copyright) : "";
    return { title: text(peek, titleStart, titleEnd), version };
}
 
/** What is in sideways `bank`: its ROM's title and version, or what it is with no ROM there. */
export function describeBank(cpu, bank) {
    const header = romHeader((address) => peekViewed(cpu, bankView(bank), address));
    if (header) return header.version ? `${header.title} ${header.version}` : header.title;
    return cpu.model.swram[bank] ? "sideways RAM" : "empty";
}