Skip to content

Commit 324e16d

Browse files
committed
WASM: UI update
1 parent 393317d commit 324e16d

File tree

5 files changed

+315
-176
lines changed

5 files changed

+315
-176
lines changed

README.md

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ network multiplayer feature, so it’s not completely useless.
1515

1616
<img src="screenshots.png" alt="Screenshots">
1717

18-
## WebAssembly Build
18+
## WebAssembly
1919

20-
WebAssembly version for modern browsers is available at https://maxpoletaev.github.io/dendy/.
21-
It runs smoothly in modern browsers, though it does not support netplay in its
22-
current form. (there was an [experimental][wasm-netplay] implementation of
23-
netplay over WebRTC, but it was too slow and unreliable to be usable).
20+
The WASM-compiled version for modern browsers is available at
21+
https://maxpoletaev.github.io/dendy/. It runs surprisingly smooth, though it does
22+
not support netplay in its current form (there was an [experimental][wasm-netplay]
23+
implementation of it over WebRTC, but it was too slow and unreliable to be
24+
usable).
2425

2526
[wasm-netplay]: https://drive.google.com/file/d/1r3ZY20L168u3djRMWA_KLMrY0eIr1ify/view?usp=sharing
2627

@@ -262,42 +263,21 @@ nescartdb.com.
262263

263264
## Dependencies
264265

265-
* https://github.com/gen2brain/raylib-go/raylib - Go bindings for raylib (graphics/audio)
266+
* https://github.com/gen2brain/raylib-go/raylib - Go bindings for Raylib (graphics/audio)
266267
* https://github.com/xtaci/kcp-go - TCP-over-UDP for netplay
267268

268269
## Resources
269270

270-
Although NES emulation is a pretty well-covered topic, It is still a very
271-
interesting and challenging project to work on. Here are some of the resources
272-
that I found particularly useful while writing this emulator. Big thanks to
273-
everyone who made them!
274-
275-
### Documentation
276-
277271
* [NESDev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki)
278-
* [MOS 6502 CPU Reference](https://web.archive.org/web/20210429110213/http://obelisk.me.uk/6502/) by Andrew Jabobs, 2009
279-
* [Extra Instructions of the 65xx Series CPU](http://www.ffd2.com/fridge/docs/6502-NMOS.extra.opcodes) by Adam Vardy, 1996
280-
* [NES Rendering Overview](https://austinmorlan.com/posts/nes_rendering_overview/) by Austin Morlan, 2019
281-
* [Making NES Games in Assembly](https://famicom.party/book/) by Kevin Zurawel, 2021
272+
* [MOS 6502 CPU Reference](https://web.archive.org/web/20210429110213/http://obelisk.me.uk/6502/) by Andrew Jabobs
273+
* [Extra Instructions of the 65xx Series CPU](http://www.ffd2.com/fridge/docs/6502-NMOS.extra.opcodes) by Adam Vardy
274+
* [NES Rendering Overview](https://austinmorlan.com/posts/nes_rendering_overview/) by Austin Morlan
275+
* [Making NES Games in Assembly](https://famicom.party/book/) by Kevin Zurawel
276+
* [NES Emulator from Scratch](https://www.youtube.com/playlist?list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf) series by javidx9
277+
* [Audio pseudo-code](https://forums.nesdev.org/viewtopic.php?t=13767) by oRBIT2002
282278
* [Retroarch Netplay README](https://github.com/libretro/RetroArch/blob/master/network/netplay/README)
283-
* [Audio pseudo-code](https://forums.nesdev.org/viewtopic.php?t=13767)
284-
285-
### Videos
286-
287-
* The [NES Emulator from Scratch][nesemu] series covers most of the topics from
288-
the CPU to the sound, but I found the two videos about the PPU to be the most
289-
useful for understanding the obscure details of the NES rendering pipeline:
290-
[[1]][ppu1], [[2]][ppu2].
291-
292-
[nesemu]: https://www.youtube.com/playlist?list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf
293-
[ppu1]: https://www.youtube.com/watch?v=-THeUXqR3zY&list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf&index=5
294-
[ppu2]: https://www.youtube.com/watch?v=cksywUTZxlY&list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf&index=6
295-
296-
### Code
297279

298-
During bad times, it’s always nice to look at other people’s code to see how
299-
they solved the same problems. Here are some of the emulators written by other
300-
people that I often referred to when I was stuck:
280+
## Referenced Projects
301281

302282
* [github.com/OneLoneCoder/olcNES](https://github.com/OneLoneCoder/olcNES)
303283
* [github.com/ad-sho-loko/goones](https://github.com/ad-sho-loko/goones)

cmd/dendy-wasm/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
const (
17-
audioBufferSize = 512
17+
audioBufferSize = 1024 // must be multiple 128 as JS consumes samples in 128 chunks
1818
)
1919

2020
//go:embed nestest.nes

web/index.html

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,68 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1">
5+
<meta name="viewport" content="width=device-width, initial-scale=0.6">
66
<title>Dendy Emulator (WASM)</title>
77
<link rel="stylesheet" href="style.css">
88
<script src="wasm_exec.js"></script>
99
<script src="main.js"></script>
1010
</head>
1111
<body>
12-
1312
<div class="container">
14-
<div class="game-section">
15-
<div class="game-window">
16-
<div class="unmute" id="unmute-button" style="display:none;">🔇 Click anywhere to unmute</div>
17-
<canvas id="canvas"></canvas>
13+
<div class="console">
14+
<div class="console__screen">
15+
<div class="screen">
16+
<canvas id="canvas" class="screen__canvas"></canvas>
17+
<div class="screen__unmute" id="unmute-button" style="display:none;">🔇 Click anywhere to unmute</div>
18+
</div>
1819
</div>
19-
</div>
20-
21-
<div class="info-section">
22-
<div class="rom-upload">
23-
Select ROM (.nes):
24-
<input type="file" id="file-input" accept=".nes">
20+
<div class="console__rom">
21+
<label class="rom-select" for="file-input">
22+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-upload mr-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" x2="12" y1="3" y2="15"></line></svg>
23+
<span class="rom-select__text">Select ROM (.nes)</span>
24+
<input type="file" id="file-input" accept=".nes" style="display: none;">
25+
</label>
2526
</div>
26-
27-
<div class="controls-grid">
28-
<div class="control-item">
29-
<span class="control-key">WASD</span>
30-
<span class="control-action">D-Pad</span>
31-
</div>
32-
<div class="control-item">
33-
<span class="control-key">J</span>
34-
<span class="control-action">B Button</span>
35-
</div>
36-
<div class="control-item">
37-
<span class="control-key">K</span>
38-
<span class="control-action">A Button</span>
39-
</div>
40-
<div class="control-item">
41-
<span class="control-key">Enter</span>
42-
<span class="control-action">Start</span>
43-
</div>
44-
<div class="control-item">
45-
<span class="control-key">Right Shift</span>
46-
<span class="control-action">Select</span>
47-
</div>
48-
<div class="control-item">
49-
<span class="control-key">⌘+R</span>
50-
<span class="control-action">Reset</span>
27+
<div class="console__controls">
28+
<div class="controls">
29+
<div class="controls__dpad">
30+
<div class="dpad">
31+
<div class="dpad__vertical">
32+
<div class="dpad__button -up" id="dpad-up">W</div>
33+
<div class="dpad__button -down" id="dpad-down">S</div>
34+
</div>
35+
<div class="dpad__horizontal">
36+
<div class="dpad__button -left" id="dpad-left">A</div>
37+
<div class="dpad__button -right" id="dpad-right">D</div>
38+
</div>
39+
<div class="dpad__border -vert"></div>
40+
<div class="dpad__border -horiz"></div>
41+
<div class="dpad__circle"></div>
42+
</div>
43+
</div>
44+
<div class="controls__center">
45+
<div class="center-buttons">
46+
<div class="center-buttons__button -select" id="button-select">
47+
<div class="center-buttons__label">RShift</div>
48+
</div>
49+
<div class="center-buttons__button -start" id="button-start">
50+
<div class="center-buttons__label">Enter</div>
51+
</div>
52+
</div>
53+
</div>
54+
<div class="controls__ab">
55+
<div class="ab">
56+
<div class="ab__border">
57+
<div class="ab__button -b" id="button-b">J</div>
58+
</div>
59+
<div class="ab__border">
60+
<div class="ab__button -a" id="button-a">K</div>
61+
</div>
62+
</div>
63+
</div>
5164
</div>
5265
</div>
5366
</div>
54-
5567
<div class="source-link">
5668
<a href="https://github.com/maxpoletaev/dendy" target="_blank">https://github.com/maxpoletaev/dendy</a>
5769
</div>

web/main.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ Promise.all([wasmReady, documentReady]).then(async () => {
1717
const TARGET_FPS = 60;
1818
const SCALE = 2;
1919

20-
const audioBufferSize = go.AudioBufferSize;
21-
const audioSampleRate = go.AudioSampleRate;
22-
2320
// ========================
2421
// Canvas setup
2522
// ========================
@@ -38,10 +35,10 @@ Promise.all([wasmReady, documentReady]).then(async () => {
3835
// Audio setup
3936
// ========================
4037

38+
const audioBufferSize = go.AudioBufferSize;
39+
const audioSampleRate = go.AudioSampleRate;
4140
console.log(`[INFO] audio sample rate: ${audioSampleRate}, buffer size: ${audioBufferSize}`);
42-
let audioCtx = new AudioContext({
43-
sampleRate: audioSampleRate,
44-
});
41+
let audioCtx = new AudioContext({sampleRate: audioSampleRate});
4542

4643
await audioCtx.audioWorklet.addModule("audio.js");
4744
let audioNode = new AudioWorkletNode(audioCtx, "audio-processor");
@@ -63,7 +60,6 @@ Promise.all([wasmReady, documentReady]).then(async () => {
6360
}
6461
}, {once: true});
6562

66-
6763
// ========================
6864
// Input handling
6965
// ========================
@@ -104,6 +100,25 @@ Promise.all([wasmReady, documentReady]).then(async () => {
104100
}
105101
});
106102

103+
const elementKeyMap = {
104+
"dpad-up": BUTTON_UP,
105+
"dpad-down": BUTTON_DOWN,
106+
"dpad-left": BUTTON_LEFT,
107+
"dpad-right": BUTTON_RIGHT,
108+
"button-start": BUTTON_START,
109+
"button-select": BUTTON_SELECT,
110+
"button-b": BUTTON_B,
111+
"button-a": BUTTON_A,
112+
};
113+
114+
for (let [id, mask] of Object.entries(elementKeyMap)) {
115+
let element = document.getElementById(id);
116+
element.addEventListener("mousedown", () => { buttonsPressed |= mask; });
117+
element.addEventListener("touchstart", () => { buttonsPressed |= mask; });
118+
element.addEventListener("mouseup", () => { buttonsPressed &= ~mask; });
119+
element.addEventListener("touchend", () => { buttonsPressed &= ~mask; });
120+
}
121+
107122
// ========================
108123
// ROM loading
109124
// ========================
@@ -132,6 +147,16 @@ Promise.all([wasmReady, documentReady]).then(async () => {
132147
});
133148
}
134149

150+
document.addEventListener('dragover', (e) => {
151+
e.preventDefault();
152+
});
153+
154+
document.addEventListener('drop', (e) => {
155+
e.preventDefault();
156+
fileInput.files = e.dataTransfer.files;
157+
fileInput.dispatchEvent(new Event('input'));
158+
});
159+
135160
// ========================
136161
// Game loop
137162
// ========================
@@ -152,7 +177,7 @@ Promise.all([wasmReady, documentReady]).then(async () => {
152177
let framePtr = go.GetFrameBufferPtr();
153178
let image = new ImageData(new Uint8ClampedArray(getMemoryBuffer(), framePtr, WIDTH * HEIGHT * 4), WIDTH, HEIGHT);
154179
ctx.putImageData(image, 0, 0);
155-
return
180+
return;
156181
}
157182

158183
let audioBufPtr = go.GetAudioBufferPtr();
@@ -165,14 +190,14 @@ Promise.all([wasmReady, documentReady]).then(async () => {
165190
const frameTime = 1000 / TARGET_FPS;
166191

167192
function loop() {
168-
requestAnimationFrame(loop)
193+
requestAnimationFrame(loop);
169194

170-
const now = performance.now()
171-
const elapsed = now - lastFrameTime
172-
if (elapsed < frameTime) return
195+
const now = performance.now();
196+
const elapsed = now - lastFrameTime;
197+
if (elapsed < frameTime) return;
173198

174-
const excessTime = elapsed % frameTime
175-
lastFrameTime = now - excessTime
199+
const excessTime = elapsed % frameTime;
200+
lastFrameTime = now - excessTime;
176201

177202
if (isInFocus()) {
178203
executeFrame();

0 commit comments

Comments
 (0)