@@ -6,27 +6,67 @@ const documentReady = new Promise((resolve) => {
66 }
77} ) ;
88
9- const go = new Go ( ) ;
9+ window . go = new Go ( ) ;
1010const wasmReady = WebAssembly . instantiateStreaming ( fetch ( "dendy.wasm" ) , go . importObject ) . then ( ( result ) => {
1111 go . run ( result . instance ) ;
1212} ) ;
1313
14- Promise . all ( [ wasmReady , documentReady ] ) . then ( ( ) => {
15- const width = 256 ;
16- const height = 240 ;
17- const targetFPS = 60 ;
18- const scale = 2 ;
14+ Promise . all ( [ wasmReady , documentReady ] ) . then ( async ( ) => {
15+ const WIDTH = 256 ;
16+ const HEIGHT = 240 ;
17+ const TARGET_FPS = 60 ;
18+ const SCALE = 2 ;
19+
20+ const audioBufferSize = go . AudioBufferSize ;
21+ const audioSampleRate = go . AudioSampleRate ;
22+
23+ // ========================
24+ // Canvas setup
25+ // ========================
1926
2027 let canvas = document . getElementById ( "canvas" ) ;
21- canvas . width = width ;
22- canvas . height = height ;
23- canvas . style . width = width * scale + "px" ;
24- canvas . style . height = height * scale + "px" ;
28+ canvas . width = WIDTH ;
29+ canvas . height = HEIGHT ;
30+ canvas . style . width = WIDTH * SCALE + "px" ;
31+ canvas . style . height = HEIGHT * SCALE + "px" ;
2532 canvas . style . imageRendering = "pixelated" ;
2633
2734 let ctx = canvas . getContext ( "2d" ) ;
2835 ctx . imageSmoothingEnabled = false ;
29- let buttonsPressed = 0 ;
36+
37+ // ========================
38+ // Audio setup
39+ // ========================
40+
41+ console . log ( `[INFO] audio sample rate: ${ audioSampleRate } , buffer size: ${ audioBufferSize } ` ) ;
42+ let audioCtx = new AudioContext ( {
43+ sampleRate : audioSampleRate ,
44+ } ) ;
45+
46+ await audioCtx . audioWorklet . addModule ( "audio.js" ) ;
47+ let audioNode = new AudioWorkletNode ( audioCtx , "audio-processor" ) ;
48+ audioNode . connect ( audioCtx . destination ) ;
49+
50+ // ========================
51+ // Mute/unmute button
52+ // ========================
53+
54+ let unmuteButton = document . getElementById ( "unmute-button" ) ;
55+ if ( audioCtx . state === "suspended" ) {
56+ unmuteButton . style . display = "block" ;
57+ }
58+
59+ document . addEventListener ( "click" , function ( ) {
60+ if ( audioCtx . state === "suspended" ) {
61+ unmuteButton . style . display = "none" ;
62+ audioCtx . resume ( ) ;
63+ }
64+ } , { once : true } ) ;
65+
66+
67+ // ========================
68+ // Input handling
69+ // ========================
3070
3171 const BUTTON_A = 1 << 0 ;
3272 const BUTTON_B = 1 << 1 ;
@@ -48,6 +88,8 @@ Promise.all([wasmReady, documentReady]).then(() => {
4888 "KeyK" : BUTTON_A ,
4989 } ;
5090
91+ let buttonsPressed = 0 ;
92+
5193 document . addEventListener ( "keydown" , ( event ) => {
5294 if ( keyMap [ event . code ] ) {
5395 event . preventDefault ( ) ;
@@ -62,52 +104,80 @@ Promise.all([wasmReady, documentReady]).then(() => {
62104 }
63105 } ) ;
64106
107+ // ========================
108+ // ROM loading
109+ // ========================
110+
65111 let fileInput = document . getElementById ( "file-input" ) ;
66112
67- fileInput . addEventListener ( "input" , function ( ) {
113+ fileInput . addEventListener ( "input" , function ( ) {
68114 this . files [ 0 ] . arrayBuffer ( ) . then ( ( buffer ) => {
69115 let rom = new Uint8Array ( buffer ) ;
70- let ok = uploadROM ( rom ) ;
116+ let ok = go . LoadROM ( rom ) ;
71117 if ( ! ok ) {
72- alert ( "Invalid ROM file" ) ;
118+ alert ( "Invalid or unsupported ROM file" ) ;
73119 this . value = "" ;
74120 }
75121 } ) ;
76- this . blur ( ) ;
122+ this . blur ( ) ; // Avoid re-opening file dialog when pressing Enter
77123 } ) ;
78124
79125 if ( fileInput . files . length > 0 ) {
80126 fileInput . files [ 0 ] . arrayBuffer ( ) . then ( ( buffer ) => {
81127 let rom = new Uint8Array ( buffer ) ;
82- let ok = uploadROM ( rom ) ;
128+ let ok = go . LoadROM ( rom ) ;
83129 if ( ! ok ) {
84130 fileInput . value = "" ;
85131 }
86132 } ) ;
87133 }
88134
135+ // ========================
136+ // Game loop
137+ // ========================
138+
89139 function isInFocus ( ) {
90140 return document . hasFocus ( ) && document . visibilityState === "visible" ;
91141 }
92142
93- function gameLoop ( ) {
94- let nextFrame = ( ) => {
95- let start = performance . now ( ) ;
143+ function getMemoryBuffer ( ) {
144+ return go . _inst . exports . mem ?. buffer || go . _inst . exports . memory . buffer ; // latter is for TinyGo
145+ }
146+
147+ function executeFrame ( ) {
148+ while ( true ) {
149+ let frameReady = go . RunFrame ( buttonsPressed ) ;
96150
97- if ( isInFocus ( ) ) {
98- let framePtr = runFrame ( buttonsPressed ) ;
99- let memPtr = go . _inst . exports . mem ?. buffer || go . _inst . exports . memory . buffer ; // latter is for TinyGo
100- let image = new ImageData ( new Uint8ClampedArray ( memPtr , framePtr , width * height * 4 ) , width , height ) ;
151+ if ( frameReady ) {
152+ let framePtr = go . GetFrameBufferPtr ( ) ;
153+ let image = new ImageData ( new Uint8ClampedArray ( getMemoryBuffer ( ) , framePtr , WIDTH * HEIGHT * 4 ) , WIDTH , HEIGHT ) ;
101154 ctx . putImageData ( image , 0 , 0 ) ;
155+ return
102156 }
103157
104- let elapsed = performance . now ( ) - start ;
105- let nextTimeout = Math . max ( 0 , ( 1000 / targetFPS ) - elapsed ) ;
106- setTimeout ( nextFrame , nextTimeout ) ;
107- } ;
158+ let audioBufPtr = go . GetAudioBufferPtr ( ) ;
159+ let audioBuf = new Float32Array ( getMemoryBuffer ( ) , audioBufPtr , go . AudioBufferSize ) ;
160+ audioNode . port . postMessage ( audioBuf . slice ( ) ) ;
161+ }
162+ }
163+
164+ let lastFrameTime = performance . now ( ) ;
165+ const frameTime = 1000 / TARGET_FPS ;
166+
167+ function loop ( ) {
168+ requestAnimationFrame ( loop )
108169
109- nextFrame ( ) ;
170+ const now = performance . now ( )
171+ const elapsed = now - lastFrameTime
172+ if ( elapsed < frameTime ) return
173+
174+ const excessTime = elapsed % frameTime
175+ lastFrameTime = now - excessTime
176+
177+ if ( isInFocus ( ) ) {
178+ executeFrame ( ) ;
179+ }
110180 }
111181
112- gameLoop ( ) ;
182+ requestAnimationFrame ( loop ) ;
113183} ) ;
0 commit comments