All files / src snapshot.js

96.15% Statements 50/52
92.1% Branches 35/38
100% Functions 9/9
97.95% Lines 48/49

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      2x 2x             24x                 34x 13x 13x 13x       2x                                       30x         30x 30x 60x 9x 9x 800x 800x 800x 800x 800x 8x     9x 9x   9x 9x 9x       30x                   30x 30x                     21x 1x   20x 1x   19x 1x   18x 4x 2x         16x                 13x 3594x 185x           3409x                   17x 3267x 175x 175x     175x   3092x      
import { typedArrayToBase64, base64ToTypedArray } from "./state-utils.js";
import { findModel } from "./models.js";
 
const SnapshotFormat = "jsbeeb-snapshot";
const SnapshotVersion = 3;
 
/**
 * Whether a snapshot was taken on a machine with a second processor fitted.
 * Nothing before version 3 captured tube state, so those snapshots are always host-only.
 */
export function hasCoProcessor(snapshot) {
    return !!snapshot.coProcessor;
}
 
/**
 * Check if two model names resolve to the same model (accounting for
 * synonyms and old names). Used to decide whether a page reload is
 * needed when loading a snapshot saved under a different model name.
 */
export function isSameModel(nameA, nameB) {
    if (nameA === nameB) return true;
    const a = findModel(nameA);
    const b = findModel(nameB);
    return a !== null && a === b;
}
 
// Map of TypedArray constructor names for deserialization
const TypedArrayConstructors = {
    Uint8Array,
    Uint16Array,
    Uint32Array,
    Int32Array,
    Float32Array,
    Float64Array,
};
 
/**
 * Create a snapshot of the emulator state for save-to-file.
 * Disc track pulse data is stripped — on restore, the discs are reloaded
 * from the source references in the `media` field. (The in-memory rewind
 * path uses cpu.snapshotState() directly, which retains full disc data.)
 * @param {import('./6502.js').Cpu6502} cpu
 * @param {object} model - the model definition object
 * @param {object} [media] - optional media source references (disc1, disc2)
 * @returns {object} snapshot object
 */
export function createSnapshot(cpu, model, media) {
    const state = cpu.snapshotState();
    // Strip clean disc track data from the save-to-file snapshot.
    // The FDC/drive mechanical state is kept; only clean tracks
    // (which can be reloaded from the disc image) are removed.
    // Dirty tracks (written since disc load) are kept as an overlay.
    Eif (state.fdc && state.fdc.drives) {
        for (const drive of state.fdc.drives) {
            if (drive.disc) {
                const dirtyTracks = {};
                for (const key of Object.keys(drive.disc.tracks)) {
                    const [sideStr, trackNumStr] = key.split(":");
                    const isSideUpper = sideStr === "true";
                    const trackNum = parseInt(trackNumStr, 10);
                    const dirtyKey = trackNum | (isSideUpper ? 0x100 : 0);
                    if (drive.disc._everDirtyTracks && drive.disc._everDirtyTracks.has(dirtyKey)) {
                        dirtyTracks[key] = drive.disc.tracks[key];
                    }
                }
                drive.disc.tracks = {};
                drive.disc.dirtyTracks = dirtyTracks;
                // Clean up internal-only fields not needed in serialized state
                delete drive.disc._everDirtyTracks;
                delete drive.disc._originalImageData;
                delete drive.disc._originalImageCrc32;
            }
        }
    }
    const snapshot = {
        format: SnapshotFormat,
        version: SnapshotVersion,
        model: model.name,
        // The model name cannot distinguish a Turbo from a plain Master: the co-processor is
        // emulation config rather than part of the model.
        coProcessor: cpu.hasTube,
        timestamp: new Date().toISOString(),
        state,
    };
    Iif (media) snapshot.media = media;
    return snapshot;
}
 
/**
 * Restore emulator state from a snapshot.
 * @param {import('./6502.js').Cpu6502} cpu
 * @param {object} model - the current model definition
 * @param {object} snapshot
 * @throws {Error} if the model doesn't match
 */
export function restoreSnapshot(cpu, model, snapshot) {
    if (snapshot.format !== SnapshotFormat) {
        throw new Error(`Unknown snapshot format: ${snapshot.format}`);
    }
    if (snapshot.version > SnapshotVersion) {
        throw new Error(`Snapshot version ${snapshot.version} is newer than supported version ${SnapshotVersion}`);
    }
    if (!isSameModel(snapshot.model, model.name)) {
        throw new Error(`Model mismatch: snapshot is for "${snapshot.model}" but current model is "${model.name}"`);
    }
    if (hasCoProcessor(snapshot) !== cpu.hasTube) {
        const fitted = (yes) => (yes ? "with a second processor" : "without a second processor");
        throw new Error(
            `Co-processor mismatch: snapshot was taken ${fitted(hasCoProcessor(snapshot))} ` +
                `but this machine is ${fitted(cpu.hasTube)}`,
        );
    }
    cpu.restoreState(snapshot.state);
}
 
/**
 * Serialize a snapshot to a JSON string, converting TypedArrays to base64.
 * @param {object} snapshot
 * @returns {string} JSON string
 */
export function snapshotToJSON(snapshot) {
    return JSON.stringify(snapshot, (key, value) => {
        if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
            return {
                __typedArray: true,
                type: value.constructor.name,
                data: typedArrayToBase64(value),
            };
        }
        return value;
    });
}
 
/**
 * Deserialize a snapshot from a JSON string, converting base64 back to TypedArrays.
 * @param {string} json
 * @returns {object} snapshot object
 */
export function snapshotFromJSON(json) {
    return JSON.parse(json, (key, value) => {
        if (value && value.__typedArray) {
            const Constructor = TypedArrayConstructors[value.type];
            Iif (!Constructor) {
                throw new Error(`Unknown TypedArray type: ${value.type}`);
            }
            return base64ToTypedArray(value.data, Constructor);
        }
        return value;
    });
}