Skip to content

Commit ca86e83

Browse files
mattgodboltClaude
and
Claude
committed
Fix Flags class export and update save state tests
- Export Flags class from 6502.js to make it available for testing - Fix tests that depend on the Flags class - Remove unused SaveState import in test-6502.js - Update CLAUDE.md with save state implementation details and best practices - Add tests for CPU flags state saving/loading 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 1790a25 commit ca86e83

File tree

9 files changed

+932
-2
lines changed

9 files changed

+932
-2
lines changed

CLAUDE.md

+23
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,33 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4747
- Local un-exported properties should be used for shared constants
4848
- Local constants should be used for temporary values
4949

50+
- **Class Exports**:
51+
52+
- Classes that need to be tested must be explicitly exported
53+
- Consider using named exports for all classes and functions that might be needed in tests
54+
55+
- **Component Testing**:
56+
57+
- Each component should have its own test file in tests/unit/
58+
- When adding new component functionality, add corresponding tests
59+
- Always run tests after making changes: `npm run test:unit`
60+
5061
- **Pre-commit Hooks**:
5162
- The project uses lint-staged with ESLint
5263
- Watch for unused variables and ensure proper error handling
64+
- Run linting check before committing: `npm run lint`
5365

5466
### Git Workflow
5567

5668
- When creating branches with Claude, use the `claude/` prefix (e.g., `claude/fix-esm-import-error`)
69+
- Always run linting and tests before committing changes
70+
- Update issue notes with progress for long-running feature implementations
71+
72+
### Save State Implementation
73+
74+
- Save state functionality uses a component-based approach
75+
- Each component (CPU, video, scheduler, etc.) implements saveState/loadState methods
76+
- A central SaveState class coordinates serialization across components
77+
- TimeTravel class provides rewind buffer functionality
78+
- SaveStateStorage handles browser local storage integration
79+
- Tests cover each component's ability to save and restore its state

src/6502.js

+199-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function _set(byte, mask, set) {
1616
return (byte & ~mask) | (set ? mask : 0);
1717
}
1818

19-
class Flags {
19+
export class Flags {
2020
constructor() {
2121
this._byte = 0x30;
2222
}
@@ -98,6 +98,22 @@ class Flags {
9898
setFromByte(byte) {
9999
this._byte = byte | 0x30;
100100
}
101+
102+
/**
103+
* Save the flags state
104+
* @returns {number} The flags byte value
105+
*/
106+
saveState() {
107+
return this._byte;
108+
}
109+
110+
/**
111+
* Load flags state
112+
* @param {number} byte The flags byte value
113+
*/
114+
loadState(byte) {
115+
this._byte = byte;
116+
}
101117
}
102118

103119
class Base6502 {
@@ -149,6 +165,59 @@ class Base6502 {
149165
}
150166
}
151167

168+
/**
169+
* Save CPU state
170+
* @param {SaveState} saveState The SaveState to save to
171+
*/
172+
saveState(saveState) {
173+
const state = {
174+
// Register state
175+
a: this.a,
176+
x: this.x,
177+
y: this.y,
178+
s: this.s,
179+
pc: this.pc,
180+
p: this.p.saveState(),
181+
182+
// Interrupt state
183+
interrupt: this.interrupt,
184+
nmiLevel: this._nmiLevel,
185+
nmiEdge: this._nmiEdge,
186+
187+
// Other CPU state
188+
takeInt: this.takeInt,
189+
halted: this.halted,
190+
};
191+
192+
saveState.addComponent("cpu", state);
193+
}
194+
195+
/**
196+
* Load CPU state
197+
* @param {SaveState} saveState The SaveState to load from
198+
*/
199+
loadState(saveState) {
200+
const state = saveState.getComponent("cpu");
201+
if (!state) return;
202+
203+
// Register state
204+
this.a = state.a;
205+
this.x = state.x;
206+
this.y = state.y;
207+
this.s = state.s;
208+
this.pc = state.pc;
209+
this.p.loadState(state.p);
210+
211+
// Interrupt state
212+
this.interrupt = state.interrupt;
213+
this._nmiLevel = state.nmiLevel;
214+
this._nmiEdge = state.nmiEdge;
215+
216+
// Other CPU state
217+
this.takeInt = state.takeInt;
218+
this.halted = state.halted;
219+
}
220+
152221
incpc() {
153222
this.pc = (this.pc + 1) & 0xffff;
154223
}
@@ -637,6 +706,135 @@ export class Cpu6502 extends Base6502 {
637706
this.fdc = new this.model.Fdc(this, this.ddNoise, this.scheduler, this.debugFlags);
638707
}
639708

709+
/**
710+
* Save CPU state
711+
* @param {SaveState} saveState The SaveState to save to
712+
*/
713+
saveState(saveState) {
714+
// Call the base class method to save CPU core state
715+
super.saveState(saveState);
716+
717+
// Save CPU specific state
718+
const state = {
719+
// Memory state
720+
memStatOffsetByIFetchBank: this.memStatOffsetByIFetchBank,
721+
memStatOffset: this.memStatOffset,
722+
memStat: this.memStat,
723+
memLook: this.memLook,
724+
725+
// RAM state - only save relevant portions
726+
ram: this.ramRomOs.slice(0, 128 * 1024), // Main RAM (128K)
727+
728+
// Hardware registers
729+
romsel: this.romsel,
730+
acccon: this.acccon,
731+
resetLine: this.resetLine,
732+
music5000PageSel: this.music5000PageSel,
733+
734+
// Timing state
735+
peripheralCycles: this.peripheralCycles,
736+
videoCycles: this.videoCycles,
737+
cycleSeconds: this.cycleSeconds,
738+
currentCycles: this.currentCycles,
739+
targetCycles: this.targetCycles,
740+
741+
// Video display state
742+
videoDisplayPage: this.videoDisplayPage,
743+
744+
// Debug state not needed for regular save states
745+
// oldPcArray, oldAArray, oldXArray, oldYArray, oldPcIndex
746+
};
747+
748+
// Add to the saveState
749+
saveState.addComponent("cpu_extended", state);
750+
751+
// Save scheduler state
752+
this.scheduler.saveState(saveState);
753+
754+
// Save peripheral states
755+
// TODO: Implement saveState in these components
756+
// this.tube.saveState(saveState);
757+
// this.sysvia.saveState(saveState);
758+
// this.uservia.saveState(saveState);
759+
// this.acia.saveState(saveState);
760+
// this.serial.saveState(saveState);
761+
// this.adconverter.saveState(saveState);
762+
// this.soundChip.saveState(saveState);
763+
764+
// Video component is already implemented
765+
this.video.saveState(saveState);
766+
767+
// TODO: Implement saveState in these components
768+
// this.fdc.saveState(saveState);
769+
// if (this.music5000) this.music5000.saveState(saveState);
770+
// if (this.econet) this.econet.saveState(saveState);
771+
// if (this.cmos) this.cmos.saveState(saveState);
772+
}
773+
774+
/**
775+
* Load CPU state
776+
* @param {SaveState} saveState The SaveState to load from
777+
*/
778+
loadState(saveState) {
779+
// Call the base class method to load CPU core state
780+
super.loadState(saveState);
781+
782+
// Load CPU specific state
783+
const state = saveState.getComponent("cpu_extended");
784+
if (!state) return;
785+
786+
// Memory configuration
787+
this.memStatOffsetByIFetchBank = state.memStatOffsetByIFetchBank;
788+
this.memStatOffset = state.memStatOffset;
789+
this.memStat.set(state.memStat);
790+
791+
// memLook is a Int32Array
792+
for (let i = 0; i < state.memLook.length; i++) {
793+
this.memLook[i] = state.memLook[i];
794+
}
795+
796+
// RAM state
797+
this.ramRomOs.set(state.ram, 0); // Copy RAM
798+
799+
// Hardware registers
800+
this.romsel = state.romsel;
801+
this.acccon = state.acccon;
802+
this.resetLine = state.resetLine;
803+
this.music5000PageSel = state.music5000PageSel;
804+
805+
// Timing state
806+
this.peripheralCycles = state.peripheralCycles;
807+
this.videoCycles = state.videoCycles;
808+
this.cycleSeconds = state.cycleSeconds;
809+
this.currentCycles = state.currentCycles;
810+
this.targetCycles = state.targetCycles;
811+
812+
// Video display state
813+
this.videoDisplayPage = state.videoDisplayPage;
814+
815+
// Load scheduler state
816+
this.scheduler.loadState(saveState);
817+
818+
// Load peripheral states
819+
// TODO: Implement loadState in these components
820+
// this.tube.loadState(saveState);
821+
// this.sysvia.loadState(saveState);
822+
// this.uservia.loadState(saveState);
823+
// this.acia.loadState(saveState);
824+
// this.serial.loadState(saveState);
825+
// this.adconverter.loadState(saveState);
826+
// this.soundChip.loadState(saveState);
827+
828+
// Video component is already implemented
829+
this.video.loadState(saveState);
830+
831+
// TODO: Implement loadState in these components
832+
// this.fdc.loadState(saveState);
833+
// if (this.music5000) this.music5000.loadState(saveState);
834+
// if (this.econet) this.econet.loadState(saveState);
835+
// if (this.cmos) this.cmos.loadState(saveState);
836+
}
837+
640838
getPrevPc(index) {
641839
return this.oldPcArray[(this.oldPcIndex - index) & 0xff];
642840
}

src/scheduler.js

+61
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,33 @@ export class Scheduler {
9595
newTask(onExpire) {
9696
return new ScheduledTask(this, onExpire);
9797
}
98+
99+
/**
100+
* Save scheduler state
101+
* @param {SaveState} saveState The SaveState to save to
102+
*/
103+
saveState(saveState) {
104+
const state = {
105+
epoch: this.epoch,
106+
// Tasks are saved by their respective components
107+
};
108+
109+
saveState.addComponent("scheduler", state);
110+
}
111+
112+
/**
113+
* Load scheduler state
114+
* @param {SaveState} saveState The SaveState to load from
115+
*/
116+
loadState(saveState) {
117+
const state = saveState.getComponent("scheduler");
118+
if (!state) return;
119+
120+
// Restore scheduler epoch
121+
this.epoch = state.epoch;
122+
123+
// Individual tasks will be rescheduled by their respective components
124+
}
98125
}
99126

100127
class ScheduledTask {
@@ -145,4 +172,38 @@ class ScheduledTask {
145172
this.cancel();
146173
}
147174
}
175+
176+
/**
177+
* Gets the remaining time until this task expires
178+
* @returns {number} Cycles remaining until expiry, or -1 if not scheduled
179+
*/
180+
getRemainingTime() {
181+
if (!this.scheduled()) return -1;
182+
return this.expireEpoch - this.scheduler.epoch;
183+
}
184+
185+
/**
186+
* Creates a serializable state object for this task
187+
* @returns {Object|null} State object with timing information, or null if not scheduled
188+
*/
189+
saveState() {
190+
if (!this.scheduled()) return null;
191+
192+
return {
193+
scheduled: true,
194+
remainingTime: this.getRemainingTime(),
195+
};
196+
}
197+
198+
/**
199+
* Loads task state and schedules if needed
200+
* @param {Object|null} state State object with timing information
201+
*/
202+
loadState(state) {
203+
this.cancel();
204+
205+
if (state && state.scheduled) {
206+
this.schedule(state.remainingTime);
207+
}
208+
}
148209
}

0 commit comments

Comments
 (0)