@slurrps/chippy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio/audio.d.ts +27 -0
- package/dist/chip8.d.ts +25 -0
- package/dist/chip8.js +1 -0
- package/dist/constants/cpu.constants.d.ts +1 -0
- package/dist/constants/display.constants.d.ts +2 -0
- package/dist/constants/instructions.constants.d.ts +19 -0
- package/dist/constants/keymap.constants.d.ts +37 -0
- package/dist/constants/memory.constants.d.ts +3 -0
- package/dist/constants/registers.constants.d.ts +2 -0
- package/dist/constants/sprite.constants.d.ts +3 -0
- package/dist/cpu/cpu.d.ts +16 -0
- package/dist/cpu/disassembler/disassembler.d.ts +14 -0
- package/dist/cpu/operations/executionContext.d.ts +29 -0
- package/dist/cpu/operations/operations.d.ts +4 -0
- package/dist/cpu/registers/registers.d.ts +24 -0
- package/dist/display/display.d.ts +31 -0
- package/dist/example.d.ts +1 -0
- package/dist/input/keyboard.d.ts +19 -0
- package/dist/memory/memory.d.ts +14 -0
- package/index.html +34 -0
- package/package.json +20 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare class Audio {
|
|
2
|
+
private audioContext;
|
|
3
|
+
private masterGain;
|
|
4
|
+
private oscillator;
|
|
5
|
+
private wave;
|
|
6
|
+
private volumeLevel;
|
|
7
|
+
private soundEnabled;
|
|
8
|
+
constructor();
|
|
9
|
+
/** Initialize audio context and gain node */
|
|
10
|
+
private initAudio;
|
|
11
|
+
/** Ensure AudioContext is running */
|
|
12
|
+
private ensureAudioContext;
|
|
13
|
+
/** Enable sound playback */
|
|
14
|
+
enableSound(): Promise<void>;
|
|
15
|
+
/** Disable sound playback */
|
|
16
|
+
disableSound(): void;
|
|
17
|
+
/** Mute audio */
|
|
18
|
+
mute(): void;
|
|
19
|
+
/** Unmute audio and set volume */
|
|
20
|
+
unMute(value?: number): void;
|
|
21
|
+
/** Play sound if timer > 0 */
|
|
22
|
+
playSound(timerValue: number): Promise<void>;
|
|
23
|
+
/** Set oscillator wave type */
|
|
24
|
+
setWave(type: OscillatorType): void;
|
|
25
|
+
/** Set master volume */
|
|
26
|
+
setVolume(value: number): void;
|
|
27
|
+
}
|
package/dist/chip8.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CPU } from "@/cpu/cpu";
|
|
2
|
+
import { Display } from "@/display/display";
|
|
3
|
+
import { Audio } from "@/audio/audio";
|
|
4
|
+
import { Keyboard } from "@/input/keyboard";
|
|
5
|
+
export declare class Chip8 {
|
|
6
|
+
cpu: CPU;
|
|
7
|
+
display: Display;
|
|
8
|
+
keyboard: Keyboard;
|
|
9
|
+
speaker: Audio;
|
|
10
|
+
private ctx;
|
|
11
|
+
private frameFinishedCallback?;
|
|
12
|
+
private fps;
|
|
13
|
+
private maxFps;
|
|
14
|
+
private interval;
|
|
15
|
+
private previousTime;
|
|
16
|
+
constructor();
|
|
17
|
+
/** Starts the main emulator loop */
|
|
18
|
+
run(): void;
|
|
19
|
+
/** Load a ROM into memory */
|
|
20
|
+
loadRom(romBuffer: Uint8Array): void;
|
|
21
|
+
/** Set a callback to receive rendered frame and CPU state */
|
|
22
|
+
onFrameFinished(callback: Function): void;
|
|
23
|
+
/** Main frame loop, synced to browser's requestAnimationFrame */
|
|
24
|
+
private runFrame;
|
|
25
|
+
}
|
package/dist/chip8.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=4096,s=512;class t{V=new Uint8Array(16);I=0;stack=new Uint16Array(16);SP=-1;PC=s;DT=0;ST=0;paused=!1;constructor(){this.reset()}reset(){this.V.fill(0),this.I=0,this.stack.fill(0),this.SP=-1,this.PC=s,this.DT=0,this.ST=0,this.paused=!1}nextInstruction(){this.PC+=2,console.log(`[PC] nextInstruction: PC=0x${this.PC.toString(16)}`)}stackPush(e){if(this.SP>=15)throw new Error("Stack Overflow: Attempted to push beyond stack depth.");this.SP++,this.stack[this.SP]=e,console.log(`[STACK] PUSH: SP=${this.SP}, value=0x${e.toString(16)}`)}stackPop(){if(this.SP<0)throw new Error("Stack Underflow: Attempted to pop from an empty stack.");const e=this.stack[this.SP];return this.SP--,console.log(`[STACK] POP: SP=${this.SP}, value=0x${e.toString(16)}`),e}updateTimers(){return this.DT>0&&this.DT--,this.ST>0&&(this.ST--,0===this.ST)}}const r={mask:4095},i={mask:15},a={mask:3840,shift:8},n={mask:240,shift:4},o={mask:255},h=61440,m=61455,d=[{key:2,id:"CLS",name:"CLS",mask:65535,pattern:224,arguments:[]},{key:3,id:"RET",name:"RET",mask:65535,pattern:238,arguments:[]},{key:4,id:"JP_ADDR",name:"JP",mask:h,pattern:4096,arguments:[r]},{key:5,id:"CALL_ADDR",name:"CALL",mask:h,pattern:8192,arguments:[r]},{key:6,id:"SE_VX_KK",name:"SE",mask:h,pattern:12288,arguments:[a,o]},{key:7,id:"SNE_VX_KK",name:"SNE",mask:h,pattern:16384,arguments:[a,o]},{key:8,id:"SE_VX_VY",name:"SE",mask:m,pattern:20480,arguments:[a,n]},{key:9,id:"LD_VX_KK",name:"LD",mask:h,pattern:24576,arguments:[a,o]},{key:10,id:"ADD_VX_KK",name:"ADD",mask:h,pattern:28672,arguments:[a,o]},{key:11,id:"LD_VX_VY",name:"LD",mask:m,pattern:32768,arguments:[a,n]},{key:12,id:"OR_VX_VY",name:"OR",mask:m,pattern:32769,arguments:[a,n]},{key:13,id:"AND_VX_VY",name:"AND",mask:m,pattern:32770,arguments:[a,n]},{key:14,id:"XOR_VX_VY",name:"XOR",mask:m,pattern:32771,arguments:[a,n]},{key:15,id:"ADD_VX_VY",name:"ADD",mask:m,pattern:32772,arguments:[a,n]},{key:16,id:"SUB_VX_VY",name:"SUB",mask:m,pattern:32773,arguments:[a,n]},{key:17,id:"SHR_VX_VY",name:"SHR",mask:m,pattern:32774,arguments:[a,n]},{key:18,id:"SUBN_VX_VY",name:"SUBN",mask:m,pattern:32775,arguments:[a,n]},{key:19,id:"SHL_VX_VY",name:"SHL",mask:m,pattern:32782,arguments:[a,n]},{key:20,id:"SNE_VX_VY",name:"SNE",mask:m,pattern:36864,arguments:[a,n]},{key:21,id:"LD_I_ADDR",name:"LD",mask:h,pattern:40960,arguments:[r]},{key:22,id:"JP_V0_ADDR",name:"JP",mask:h,pattern:45056,arguments:[r]},{key:23,id:"RND_VX_KK",name:"RND",mask:h,pattern:49152,arguments:[a,o]},{key:24,id:"DRW_VX_VY_N",name:"DRW",mask:h,pattern:53248,arguments:[a,n,i]},{key:25,id:"SKP_VX",name:"SKP",mask:61695,pattern:57502,arguments:[a]},{key:26,id:"SKNP_VX",name:"SKNP",mask:61695,pattern:57505,arguments:[a]},{key:27,id:"LD_VX_DT",name:"LD",mask:61695,pattern:61447,arguments:[a]},{key:28,id:"LD_VX_K",name:"LD",mask:61695,pattern:61450,arguments:[a]},{key:29,id:"LD_DT_VX",name:"LD",mask:61695,pattern:61461,arguments:[a]},{key:30,id:"LD_ST_VX",name:"LD",mask:61695,pattern:61464,arguments:[a]},{key:31,id:"ADD_I_VX",name:"ADD",mask:61695,pattern:61470,arguments:[a]},{key:32,id:"LD_F_VX",name:"LD",mask:61695,pattern:61481,arguments:[a]},{key:33,id:"LD_B_VX",name:"LD",mask:61695,pattern:61491,arguments:[a]},{key:34,id:"LD_I_VX",name:"LD",mask:61695,pattern:61525,arguments:[a]},{key:35,id:"LD_VX_I",name:"LD",mask:61695,pattern:61541,arguments:[a]},{key:36,id:"SCD nibble",name:"SCD",mask:i,pattern:192,arguments:[]}];class u{disassemble(e){try{const s=d.find(s=>(e&Number(s.mask))===s.pattern);if(!s)return console.error(`Opcode not found: 0x${e.toString(16)}`),{instruction:null,args:[]};const t=s.arguments.map(s=>{const t=s.shift??0;return(e&Number(s.mask))>>t});return{instruction:s,args:t}}catch(s){return console.error(`Error disassembling opcode: 0x${e.toString(16)}`,s),{instruction:null,args:[]}}}}const g={CLS(e){e.drawSprite(0,0,new Uint8Array)},RET(e){e.registers.PC=e.registers.stackPop()},JP_ADDR(e,[s]){e.registers.PC=s},CALL_ADDR(e,[s]){e.registers.stackPush(e.registers.PC),e.registers.PC=s},SE_VX_KK(e,[s,t]){e.registers.V[s]===t&&e.registers.nextInstruction()},SNE_VX_KK(e,[s,t]){e.registers.V[s]!==t&&e.registers.nextInstruction()},SE_VX_VY(e,[s,t]){e.registers.V[s]===e.registers.V[t]&&e.registers.nextInstruction()},LD_VX_KK(e,[s,t]){e.registers.V[s]=t},ADD_VX_KK(e,[s,t]){e.registers.V[s]+=t},LD_VX_VY(e,[s,t]){e.registers.V[s]=e.registers.V[t]},OR_VX_VY(e,[s,t]){e.registers.V[s]|=e.registers.V[t]},AND_VX_VY(e,[s,t]){e.registers.V[s]&=e.registers.V[t]},XOR_VX_VY(e,[s,t]){e.registers.V[s]^=e.registers.V[t]},ADD_VX_VY(e,[s,t]){const r=e.registers.V[s]+e.registers.V[t];e.registers.V[15]=r>255?1:0,e.registers.V[s]=255&r},SUB_VX_VY(e,[s,t]){e.registers.V[15]=e.registers.V[s]>=e.registers.V[t]?1:0,e.registers.V[s]=e.registers.V[s]-e.registers.V[t]&255},SHR_VX_VY(e,[s]){e.registers.V[15]=1&e.registers.V[s],e.registers.V[s]>>=1},SUBN_VX_VY(e,[s,t]){e.registers.V[15]=e.registers.V[t]>=e.registers.V[s]?1:0,e.registers.V[s]=e.registers.V[t]-e.registers.V[s]&255},SHL_VX_VY(e,[s]){e.registers.V[15]=(128&e.registers.V[s])>>7,e.registers.V[s]<<=1,e.registers.V[s]&=255},SNE_VX_VY(e,[s,t]){e.registers.V[s]!==e.registers.V[t]&&e.registers.nextInstruction()},LD_I_ADDR(e,[s]){e.registers.I=s},JP_V0_ADDR(e,[s]){e.registers.PC=s+e.registers.V[0]},RND_VX_KK(e,[s],t){const r=Math.floor(255*Math.random());e.registers.V[s]=255&r&t},DRW_VX_VY_N(e,[s,t],r){const i=15&r,a=e.memory.slice(e.registers.I,e.registers.I+i),n=e.drawSprite(e.registers.V[s],e.registers.V[t],a);e.registers.V[15]=n?1:0},SKP_VX(e,[s]){e.isKeyPressed(e.registers.V[s])&&e.registers.nextInstruction()},SKNP_VX(e,[s]){e.isKeyPressed(e.registers.V[s])||e.registers.nextInstruction()},LD_VX_DT(e,[s]){e.registers.V[s]=e.registers.DT},LD_VX_K(e,[s]){e.registers.paused=!0,e.waitForKeyPress(t=>{e.registers.V[s]=t,e.registers.paused=!1})},LD_DT_VX(e,[s]){e.registers.DT=e.registers.V[s]},LD_ST_VX(e,[s]){e.registers.ST=e.registers.V[s]},ADD_I_VX(e,[s]){e.registers.I+=e.registers.V[s]},LD_F_VX(e,[s]){e.registers.I=5*e.registers.V[s]},LD_B_VX(e,[s]){const t=e.registers.V[s];e.memory.write(e.registers.I,Math.floor(t/100)),e.memory.write(e.registers.I+1,Math.floor(t%100/10)),e.memory.write(e.registers.I+2,t%10)},LD_I_VX(e,[s]){for(let t=0;t<=s;t++)e.memory.write(e.registers.I+t,e.registers.V[t])},LD_VX_I(e,[s]){for(let t=0;t<=s;t++)e.registers.V[t]=e.memory.read(e.registers.I+t)}};class l{registers=new t;disassembler=new u;tick(e){if(this.registers.paused)return 0;const s=e.memory.readOpcode(this.registers.PC);this.registers.nextInstruction();const{instruction:t,args:r}=this.disassembler.disassemble(s);if(!t)return console.error(`Invalid opcode 0x${s.toString(16)}`),0;const i=g[t.id];return i?(i(e,r,s),e.decrementTimers(),1):(console.error(`Unimplemented instruction: ${t.id}`),0)}reset(){this.registers.reset()}}const c=[240,144,144,144,240,32,96,32,32,112,240,16,240,128,240,240,16,240,16,240,144,144,240,16,16,240,128,240,16,240,240,128,240,144,240,240,16,32,64,64,240,144,240,144,240,240,144,240,16,240,240,144,240,144,144,224,144,224,144,224,240,128,128,128,240,224,144,144,144,224,240,128,240,128,240,240,128,240,128,128];class V extends ImageData{setPixel(e,s,t,r,i,a=255){if(e<0||e>=this.width||s<0||s>=this.height)return;const n=s*this.width*4+4*e;this.data[n]=t,this.data[n+1]=r,this.data[n+2]=i,this.data[n+3]=a}}class _{width=64;height=32;scale;scaledWidth;scaledHeight;frameBuffer;foregroundColor=[255,255,255];backgroundColor=[0,0,0];constructor(e=1){this.scale=e,this.scaledWidth=this.width*this.scale,this.scaledHeight=this.height*this.scale,this.frameBuffer=new V(this.scaledWidth,this.scaledHeight),this.clear()}setForegroundColor(e){this.foregroundColor=this.parseColor(e)}setBackgroundColor(e){this.backgroundColor=this.parseColor(e)}clear(){for(let e=0;e<this.scaledHeight;e++)for(let s=0;s<this.scaledWidth;s++)this.frameBuffer.setPixel(s,e,this.backgroundColor[0],this.backgroundColor[1],this.backgroundColor[2],255)}drawSprite(e,s,t){let r=!1;for(let i=0;i<t.length;i++){let a=t[i];for(let t=0;t<8;t++){if((128&a)>0){const a=(e+t)%this.width,n=(s+i)%this.height;let o=!1;for(let e=0;e<this.scale;e++)for(let s=0;s<this.scale;s++){const t=a*this.scale+s,r=4*((n*this.scale+e)*this.scaledWidth+t),i=this.frameBuffer.data[r]>0?1:0,h=1^i;this.frameBuffer.data[r]=h*this.foregroundColor[0],this.frameBuffer.data[r+1]=h*this.foregroundColor[1],this.frameBuffer.data[r+2]=h*this.foregroundColor[2],this.frameBuffer.data[r+3]=255,1===i&&0===h&&(o=!0)}o&&(r=!0)}a<<=1}}return r}parseColor(e){if("string"==typeof e){3===(e=e.replace(/^#/,"")).length&&(e=e.split("").map(e=>e+e).join(""));const s=parseInt(e,16);return[s>>16&255,s>>8&255,255&s]}return e}}const k=new class{memory=new Uint8Array(e);romLength=0;constructor(){this.reset()}read(e){return this.assertMemory(e),this.memory[e]}write(e,s){this.assertMemory(e),this.memory[e]=255&s}slice(e,s){return this.memory.slice(e,s)}readOpcode(e){return this.isAddressInROM(e)?this.read(e)<<8|this.read(e+1):(console.warn(`PC 0x${e.toString(16)} out of ROM bounds, returning 0x0000`),0)}reset(){this.memory.fill(0),this.memory.set(c,0)}loadROM(t){if(t.length+s>e)throw new Error("ROM size exceeds memory capacity");this.memory.set(t,s),this.romLength=t.length}isAddressInROM(e){return e>=s&&e<s+this.romLength}assertMemory(s){if(s<0||s>=e)throw new Error(`Memory access out of bounds at 0x${s.toString(16)}`)}};class y{audioContext=null;masterGain=null;oscillator=null;wave="square";volumeLevel=.3;soundEnabled=!1;constructor(){this.initAudio()}initAudio(){const e=window.AudioContext||window.webkitAudioContext;e&&(this.audioContext=new e,this.masterGain=this.audioContext.createGain(),this.masterGain.connect(this.audioContext.destination),this.masterGain.gain.value=this.volumeLevel)}async ensureAudioContext(){this.audioContext&&"suspended"===this.audioContext.state&&await this.audioContext.resume()}async enableSound(){this.audioContext&&(await this.ensureAudioContext(),this.soundEnabled||(this.oscillator=new OscillatorNode(this.audioContext,{type:this.wave}),this.oscillator.connect(this.masterGain),this.oscillator.start(),this.soundEnabled=!0))}disableSound(){this.oscillator&&this.soundEnabled&&(this.oscillator.stop(),this.oscillator.disconnect(),this.oscillator=null,this.soundEnabled=!1)}mute(){this.volumeLevel=0,this.masterGain&&(this.masterGain.gain.value=0)}unMute(e=.3){this.volumeLevel=e,this.masterGain&&(this.masterGain.gain.value=e)}async playSound(e){e>0?await this.enableSound():this.disableSound()}setWave(e){this.wave=e,this.oscillator&&(this.oscillator.type=e)}setVolume(e){this.volumeLevel=e,this.masterGain&&(this.masterGain.gain.value=e)}}const p={49:1,50:2,51:3,52:12,81:4,87:5,69:6,82:13,65:7,83:8,68:9,70:14,90:10,88:0,67:11,86:15};class D{keyPressed=new Array(16).fill(!1);onNextKeyPress=null;constructor(){window.addEventListener("keydown",e=>this.onKeyDown(e)),window.addEventListener("keyup",e=>this.onKeyUp(e))}isKeyPressed(e){return this.keyPressed[e]||!1}onKeyDown(e){const s=p[String(e.keyCode)];void 0!==s&&(this.keyPressed[s]=!0)}onKeyUp(e){const s=p[String(e.keyCode)];void 0!==s&&(this.keyPressed[s]=!1,this.onNextKeyPress&&(this.onNextKeyPress(s),this.onNextKeyPress=null))}triggerKeyDown(e){this.keyPressed[e]=!0}triggerKeyUp(e){this.keyPressed[e]=!1,this.onNextKeyPress&&(this.onNextKeyPress(e),this.onNextKeyPress=null)}waitForNextKeyPress(e){this.onNextKeyPress=e}}class S{cpu;display;keyboard;speaker;ctx;frameFinishedCallback;fps=0;maxFps=60;interval=1e3/this.maxFps;previousTime=0;constructor(){this.display=new _(10),this.keyboard=new D,this.speaker=new y,this.cpu=new l,this.ctx={registers:this.cpu.registers,memory:k,drawSprite:(e,s,t)=>this.display.drawSprite(e,s,t),isKeyPressed:e=>this.keyboard.isKeyPressed(e),waitForKeyPress:e=>this.keyboard.onNextKeyPress=e,decrementTimers:()=>this.cpu.registers.updateTimers()}}run(){requestAnimationFrame(e=>this.runFrame(e))}loadRom(e){this.ctx.memory.reset(),this.display.clear(),this.cpu.registers.reset(),k.loadROM(e)}onFrameFinished(e){this.frameFinishedCallback=e}runFrame(e){this.previousTime||(this.previousTime=e);const s=e-this.previousTime;if(s>=this.interval){this.fps=1e3/s,this.previousTime=e-s%this.interval;const t=10;for(let e=0;e<t;e++)this.cpu.tick(this.ctx),this.speaker.playSound(this.cpu.registers.ST);this.frameFinishedCallback&&this.frameFinishedCallback(this.display.frameBuffer,this.fps,this.cpu.registers)}requestAnimationFrame(e=>this.runFrame(e))}}export{S as Chip8};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CHIP8_SPEED = 10;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const INSTRUCTION_SET: ({
|
|
2
|
+
key: number;
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
mask: number;
|
|
6
|
+
pattern: number;
|
|
7
|
+
arguments: {
|
|
8
|
+
mask: number;
|
|
9
|
+
}[];
|
|
10
|
+
} | {
|
|
11
|
+
key: number;
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
mask: {
|
|
15
|
+
mask: number;
|
|
16
|
+
};
|
|
17
|
+
pattern: number;
|
|
18
|
+
arguments: never[];
|
|
19
|
+
})[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const NUMBER_OF_KEYS = 16;
|
|
2
|
+
export declare const KEYMAP: {
|
|
3
|
+
49: number;
|
|
4
|
+
50: number;
|
|
5
|
+
51: number;
|
|
6
|
+
52: number;
|
|
7
|
+
81: number;
|
|
8
|
+
87: number;
|
|
9
|
+
69: number;
|
|
10
|
+
82: number;
|
|
11
|
+
65: number;
|
|
12
|
+
83: number;
|
|
13
|
+
68: number;
|
|
14
|
+
70: number;
|
|
15
|
+
90: number;
|
|
16
|
+
88: number;
|
|
17
|
+
67: number;
|
|
18
|
+
86: number;
|
|
19
|
+
};
|
|
20
|
+
export declare const DigitalKeyMapping: {
|
|
21
|
+
key1: number;
|
|
22
|
+
key2: number;
|
|
23
|
+
key3: number;
|
|
24
|
+
keyC: number;
|
|
25
|
+
key4: number;
|
|
26
|
+
key5: number;
|
|
27
|
+
key6: number;
|
|
28
|
+
keyD: number;
|
|
29
|
+
key7: number;
|
|
30
|
+
key8: number;
|
|
31
|
+
key9: number;
|
|
32
|
+
keyE: number;
|
|
33
|
+
keyA: number;
|
|
34
|
+
key0: number;
|
|
35
|
+
keyB: number;
|
|
36
|
+
keyF: number;
|
|
37
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Registers } from "@/cpu/registers/registers";
|
|
2
|
+
import type { ExecutionContext } from "@/cpu/operations/executionContext";
|
|
3
|
+
export declare class CPU {
|
|
4
|
+
registers: Registers;
|
|
5
|
+
private disassembler;
|
|
6
|
+
/**
|
|
7
|
+
* Execute one CPU tick (instruction cycle)
|
|
8
|
+
* @param ctx The execution context (memory, registers, I/O)
|
|
9
|
+
* @returns Number of instructions executed (1 per tick)
|
|
10
|
+
*/
|
|
11
|
+
tick(ctx: ExecutionContext): number;
|
|
12
|
+
/**
|
|
13
|
+
* Reset CPU registers
|
|
14
|
+
*/
|
|
15
|
+
reset(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disassembler converts a 16-bit opcode into a Chip8 instruction and its arguments.
|
|
3
|
+
*/
|
|
4
|
+
export declare class Disassembler {
|
|
5
|
+
/**
|
|
6
|
+
* Disassemble a given opcode
|
|
7
|
+
* @param opcode 16-bit opcode
|
|
8
|
+
* @returns { instruction: Instruction | null, args: number[] }
|
|
9
|
+
*/
|
|
10
|
+
disassemble(opcode: number): {
|
|
11
|
+
instruction: any | null;
|
|
12
|
+
args: number[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Registers } from "@/cpu/registers/registers";
|
|
2
|
+
import { Memory } from "@/memory/memory";
|
|
3
|
+
/**
|
|
4
|
+
* ExecutionContext provides the CPU with access to memory, registers,
|
|
5
|
+
* display, input, and timers. It abstracts the underlying hardware
|
|
6
|
+
* so that the CPU can operate independently.
|
|
7
|
+
*/
|
|
8
|
+
export interface ExecutionContext {
|
|
9
|
+
/** CPU registers */
|
|
10
|
+
registers: Registers;
|
|
11
|
+
/** System memory */
|
|
12
|
+
memory: Memory;
|
|
13
|
+
/**
|
|
14
|
+
* Draw a sprite at (x, y). Returns true if any pixels were flipped from set to unset (collision).
|
|
15
|
+
* @param x - X coordinate
|
|
16
|
+
* @param y - Y coordinate
|
|
17
|
+
* @param sprite - Uint8Array of sprite bytes
|
|
18
|
+
*/
|
|
19
|
+
drawSprite(x: number, y: number, sprite: Uint8Array): boolean;
|
|
20
|
+
/** Check if a key is currently pressed */
|
|
21
|
+
isKeyPressed(key: number): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Wait for the next key press.
|
|
24
|
+
* @param callback - called with the key value once pressed
|
|
25
|
+
*/
|
|
26
|
+
waitForKeyPress(callback: (key: number) => void): void;
|
|
27
|
+
/** Called each cycle to decrement delay and sound timers */
|
|
28
|
+
decrementTimers(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare class Registers {
|
|
2
|
+
V: Uint8Array;
|
|
3
|
+
I: number;
|
|
4
|
+
stack: Uint16Array;
|
|
5
|
+
SP: number;
|
|
6
|
+
PC: number;
|
|
7
|
+
DT: number;
|
|
8
|
+
ST: number;
|
|
9
|
+
paused: boolean;
|
|
10
|
+
constructor();
|
|
11
|
+
/** Reset all registers, stack, PC, timers, and pause flag */
|
|
12
|
+
reset(): void;
|
|
13
|
+
/** Increment program counter to next instruction (2 bytes per CHIP-8 instruction) */
|
|
14
|
+
nextInstruction(): void;
|
|
15
|
+
/** Push value onto stack */
|
|
16
|
+
stackPush(value: number): void;
|
|
17
|
+
/** Pop value from stack */
|
|
18
|
+
stackPop(): number;
|
|
19
|
+
/**
|
|
20
|
+
* Update timers (called each cycle at ~60Hz)
|
|
21
|
+
* Decrements DT and ST, returns true if ST reached 0 (useful for beeps)
|
|
22
|
+
*/
|
|
23
|
+
updateTimers(): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced ImageData with a setPixel helper
|
|
3
|
+
*/
|
|
4
|
+
export declare class EnhancedImageData extends ImageData {
|
|
5
|
+
setPixel(x: number, y: number, red: number, green: number, blue: number, alpha?: number): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Display class for Chip8
|
|
9
|
+
*/
|
|
10
|
+
export declare class Display {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
scale: number;
|
|
14
|
+
scaledWidth: number;
|
|
15
|
+
scaledHeight: number;
|
|
16
|
+
frameBuffer: EnhancedImageData;
|
|
17
|
+
foregroundColor: [number, number, number];
|
|
18
|
+
backgroundColor: [number, number, number];
|
|
19
|
+
constructor(scale?: number);
|
|
20
|
+
setForegroundColor(color: string | [number, number, number]): void;
|
|
21
|
+
setBackgroundColor(color: string | [number, number, number]): void;
|
|
22
|
+
/** Clear the screen */
|
|
23
|
+
clear(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Draw a sprite at x, y. Returns true if any pixel was erased (collision)
|
|
26
|
+
* Each sprite row is a byte (8 pixels)
|
|
27
|
+
* Draws each CHIP-8 pixel as a block of size scale x scale
|
|
28
|
+
*/
|
|
29
|
+
drawSprite(x: number, y: number, sprite: Uint8Array): boolean;
|
|
30
|
+
private parseColor;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class Keyboard {
|
|
2
|
+
/** Array of pressed keys */
|
|
3
|
+
keyPressed: boolean[];
|
|
4
|
+
/** Callback for waiting for next key press */
|
|
5
|
+
onNextKeyPress: ((key: number) => void) | null;
|
|
6
|
+
constructor();
|
|
7
|
+
/** Checks if a key is pressed */
|
|
8
|
+
isKeyPressed(keyCode: number): boolean;
|
|
9
|
+
/** Handle physical key down */
|
|
10
|
+
private onKeyDown;
|
|
11
|
+
/** Handle physical key up */
|
|
12
|
+
private onKeyUp;
|
|
13
|
+
/** Manually trigger a key press (useful for virtual or headless input) */
|
|
14
|
+
triggerKeyDown(keyCode: number): void;
|
|
15
|
+
/** Manually trigger a key release (useful for virtual or headless input) */
|
|
16
|
+
triggerKeyUp(keyCode: number): void;
|
|
17
|
+
/** Wait for the next key press (for FX0A opcode) */
|
|
18
|
+
waitForNextKeyPress(callback: (key: number) => void): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class Memory {
|
|
2
|
+
private memory;
|
|
3
|
+
private romLength;
|
|
4
|
+
constructor();
|
|
5
|
+
read(address: number): number;
|
|
6
|
+
write(address: number, value: number): void;
|
|
7
|
+
slice(start: number, end: number): Uint8Array;
|
|
8
|
+
readOpcode(address: number): number;
|
|
9
|
+
reset(): void;
|
|
10
|
+
loadROM(buffer: Uint8Array): void;
|
|
11
|
+
isAddressInROM(address: number): boolean;
|
|
12
|
+
private assertMemory;
|
|
13
|
+
}
|
|
14
|
+
export declare const memory: Memory;
|
package/index.html
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Chip8 Emulator</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
background-color: #222;
|
|
13
|
+
color: #eee;
|
|
14
|
+
font-family: sans-serif;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
canvas {
|
|
18
|
+
image-rendering: pixelated;
|
|
19
|
+
margin-top: 20px;
|
|
20
|
+
border: 2px solid #eee;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
input {
|
|
24
|
+
margin-top: 10px;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<h1>Chip8 Emulator</h1>
|
|
30
|
+
<canvas id="screen"></canvas>
|
|
31
|
+
<input type="file" id="romLoader" accept=".ch8,.rom" />
|
|
32
|
+
<script type="module" src="/src/example.ts"></script>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slurrps/chippy",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "a Chip8 emulator written in typescript",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "webpack",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/node": "^25.0.3",
|
|
14
|
+
"ts-loader": "^9.5.4",
|
|
15
|
+
"typescript": "~5.9.3",
|
|
16
|
+
"vite": "^7.2.4",
|
|
17
|
+
"webpack": "^5.104.1",
|
|
18
|
+
"webpack-cli": "^6.0.1"
|
|
19
|
+
}
|
|
20
|
+
}
|