diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4e4e9413917f44ce4203828cbca155191fb7c3..913906d133d00c6ac97f57340df9ddb54ba9f37a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for true fullscreen at a browser level * Support for more flexible palette colors * Support for setting palette colors using WASM +* Local storage usage for saving battery backed RAM ### Changed diff --git a/examples/web/gb.ts b/examples/web/gb.ts index a34c1e6bd57bc7c9d6a8e8b15591d9bab5bfafba..edbf4dcd0ed0ca0834ea441e426963271d3b9a04 100644 --- a/examples/web/gb.ts +++ b/examples/web/gb.ts @@ -15,13 +15,22 @@ import { PpuMode } from "./lib/boytacean.js"; import info from "./package.json"; +import { base64ToBuffer, bufferToBase64 } from "./util"; declare const require: any; const LOGIC_HZ = 4194304; + const VISUAL_HZ = 59.7275; const IDLE_HZ = 10; +/** + * The rate at which the local storage RAM state flush + * operation is going to be performed, this value is the + * number of seconds in between flush operations (eg: 5 seconds). + */ +const STORE_RATE = 5; + const SAMPLE_RATE = 2; const KEYS_NAME: Record<string, number> = { @@ -65,6 +74,7 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { private frameStart: number = new Date().getTime(); private frameCount: number = 0; private paletteIndex: number = 0; + private storeCycles: number = LOGIC_HZ * STORE_RATE; private romName: string | null = null; private romData: Uint8Array | null = null; @@ -190,7 +200,8 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // runs the Game Boy clock, this operations should // include the advance of both the CPU and the PPU - counterCycles += this.gameBoy?.clock() ?? 0; + const tickCycles = this.gameBoy?.clock() ?? 0; + counterCycles += tickCycles; // in case the current PPU mode is VBlank and the // frame is different from the previously rendered @@ -206,13 +217,16 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { // triggers the frame event indicating that // a new frame is now available for drawing this.trigger("frame"); + } - // @todo this has to be structureed in a better way - if (this.cartridge && this.cartridge.has_battery()) { - const ramData = this.cartridge.ram_data_eager(); - const decoder = new TextDecoder("utf8"); - const ramDataB64 = btoa(decoder.decode(ramData)); - localStorage.setItem(this.cartridge.title(), ramDataB64) + // in case the current cartridge is battery backed + // then we need to check if a RAM flush to local + // storage operation is required + if (this.cartridge && this.cartridge.has_battery()) { + this.storeCycles -= tickCycles; + if (this.storeCycles <= 0) { + this.storeRam(); + this.storeCycles = this.logicFrequency * STORE_RATE; } } } @@ -299,22 +313,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { this.gameBoy.load_boot_default(); const cartridge = this.gameBoy.load_rom_ws(romData!); - // in case there's a battery involved tries to obtain - if (cartridge.has_battery()) { - } + // updates the name of the currently selected engine + // to the one that has been provided (logic change) + if (engine) this._engine = engine; // updates the ROM name in case there's extra information // coming from the cartridge romName = cartridge.title() ? cartridge.title() : romName; - // updates the name of the currently selected engine - // to the one that has been provided (logic change) - if (engine) this._engine = engine; - // updates the complete set of global information that // is going to be displayed this.setRom(romName!, romData!, cartridge); + // in case there's a battery involved tries to load the + // current RAM from the local storage + if (cartridge.has_battery()) this.loadRam(); + // in case the restore (state) flag is set // then resumes the machine execution if (restore) this.resume(); @@ -504,6 +518,22 @@ export class GameboyEmulator extends EmulatorBase implements Emulator { } } + private loadRam() { + if (!this.gameBoy || !this.cartridge) return; + const ramDataB64 = localStorage.getItem(this.cartridge.title()); + if (!ramDataB64) return; + const ramData = base64ToBuffer(ramDataB64); + this.gameBoy.set_ram_data(ramData); + } + + private storeRam() { + if (!this.gameBoy || !this.cartridge) return; + const title = this.cartridge.title(); + const ramData = this.gameBoy.ram_data_eager(); + const ramDataB64 = bufferToBase64(ramData); + localStorage.setItem(title, ramDataB64); + } + private setPalette(index?: number) { index ??= this.paletteIndex; const palette = PALETTES[index]; diff --git a/examples/web/util.ts b/examples/web/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..d52196c025576237af8c49dd7a1f98a18f56ec4f --- /dev/null +++ b/examples/web/util.ts @@ -0,0 +1,18 @@ +export const bufferToBase64 = (buffer: Uint8Array) => { + const array = Array(buffer.length) + .fill("") + .map((_, i) => String.fromCharCode(buffer[i])) + .join(""); + const base64 = btoa(array); + return base64; +}; + +export const base64ToBuffer = (base64: string) => { + const data = window.atob(base64); + const length = data.length; + const buffer = new Uint8Array(length); + for (let i = 0; i < length; i++) { + buffer[i] = data.charCodeAt(i); + } + return buffer; +};