Skip to content

Commit

Permalink
WASM: UI update
Browse files Browse the repository at this point in the history
  • Loading branch information
maxpoletaev committed Jan 11, 2025
1 parent 393317d commit 324e16d
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 176 deletions.
48 changes: 14 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ network multiplayer feature, so it’s not completely useless.

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

## WebAssembly Build
## WebAssembly

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

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

Expand Down Expand Up @@ -262,42 +263,21 @@ nescartdb.com.

## Dependencies

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

## Resources

Although NES emulation is a pretty well-covered topic, It is still a very
interesting and challenging project to work on. Here are some of the resources
that I found particularly useful while writing this emulator. Big thanks to
everyone who made them!

### Documentation

* [NESDev Wiki](https://www.nesdev.org/wiki/Nesdev_Wiki)
* [MOS 6502 CPU Reference](https://web.archive.org/web/20210429110213/http://obelisk.me.uk/6502/) by Andrew Jabobs, 2009
* [Extra Instructions of the 65xx Series CPU](http://www.ffd2.com/fridge/docs/6502-NMOS.extra.opcodes) by Adam Vardy, 1996
* [NES Rendering Overview](https://austinmorlan.com/posts/nes_rendering_overview/) by Austin Morlan, 2019
* [Making NES Games in Assembly](https://famicom.party/book/) by Kevin Zurawel, 2021
* [MOS 6502 CPU Reference](https://web.archive.org/web/20210429110213/http://obelisk.me.uk/6502/) by Andrew Jabobs
* [Extra Instructions of the 65xx Series CPU](http://www.ffd2.com/fridge/docs/6502-NMOS.extra.opcodes) by Adam Vardy
* [NES Rendering Overview](https://austinmorlan.com/posts/nes_rendering_overview/) by Austin Morlan
* [Making NES Games in Assembly](https://famicom.party/book/) by Kevin Zurawel
* [NES Emulator from Scratch](https://www.youtube.com/playlist?list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf) series by javidx9
* [Audio pseudo-code](https://forums.nesdev.org/viewtopic.php?t=13767) by oRBIT2002
* [Retroarch Netplay README](https://github.com/libretro/RetroArch/blob/master/network/netplay/README)
* [Audio pseudo-code](https://forums.nesdev.org/viewtopic.php?t=13767)

### Videos

* The [NES Emulator from Scratch][nesemu] series covers most of the topics from
the CPU to the sound, but I found the two videos about the PPU to be the most
useful for understanding the obscure details of the NES rendering pipeline:
[[1]][ppu1], [[2]][ppu2].

[nesemu]: https://www.youtube.com/playlist?list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf
[ppu1]: https://www.youtube.com/watch?v=-THeUXqR3zY&list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf&index=5
[ppu2]: https://www.youtube.com/watch?v=cksywUTZxlY&list=PLrOv9FMX8xJHqMvSGB_9G9nZZ_4IgteYf&index=6

### Code

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

* [github.com/OneLoneCoder/olcNES](https://github.com/OneLoneCoder/olcNES)
* [github.com/ad-sho-loko/goones](https://github.com/ad-sho-loko/goones)
Expand Down
2 changes: 1 addition & 1 deletion cmd/dendy-wasm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

const (
audioBufferSize = 512
audioBufferSize = 1024 // must be multiple 128 as JS consumes samples in 128 chunks
)

//go:embed nestest.nes
Expand Down
88 changes: 50 additions & 38 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,68 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=0.6">
<title>Dendy Emulator (WASM)</title>
<link rel="stylesheet" href="style.css">
<script src="wasm_exec.js"></script>
<script src="main.js"></script>
</head>
<body>

<div class="container">
<div class="game-section">
<div class="game-window">
<div class="unmute" id="unmute-button" style="display:none;">🔇 Click anywhere to unmute</div>
<canvas id="canvas"></canvas>
<div class="console">
<div class="console__screen">
<div class="screen">
<canvas id="canvas" class="screen__canvas"></canvas>
<div class="screen__unmute" id="unmute-button" style="display:none;">🔇 Click anywhere to unmute</div>
</div>
</div>
</div>

<div class="info-section">
<div class="rom-upload">
Select ROM (.nes):
<input type="file" id="file-input" accept=".nes">
<div class="console__rom">
<label class="rom-select" for="file-input">
<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>
<span class="rom-select__text">Select ROM (.nes)</span>
<input type="file" id="file-input" accept=".nes" style="display: none;">
</label>
</div>

<div class="controls-grid">
<div class="control-item">
<span class="control-key">WASD</span>
<span class="control-action">D-Pad</span>
</div>
<div class="control-item">
<span class="control-key">J</span>
<span class="control-action">B Button</span>
</div>
<div class="control-item">
<span class="control-key">K</span>
<span class="control-action">A Button</span>
</div>
<div class="control-item">
<span class="control-key">Enter</span>
<span class="control-action">Start</span>
</div>
<div class="control-item">
<span class="control-key">Right Shift</span>
<span class="control-action">Select</span>
</div>
<div class="control-item">
<span class="control-key">⌘+R</span>
<span class="control-action">Reset</span>
<div class="console__controls">
<div class="controls">
<div class="controls__dpad">
<div class="dpad">
<div class="dpad__vertical">
<div class="dpad__button -up" id="dpad-up">W</div>
<div class="dpad__button -down" id="dpad-down">S</div>
</div>
<div class="dpad__horizontal">
<div class="dpad__button -left" id="dpad-left">A</div>
<div class="dpad__button -right" id="dpad-right">D</div>
</div>
<div class="dpad__border -vert"></div>
<div class="dpad__border -horiz"></div>
<div class="dpad__circle"></div>
</div>
</div>
<div class="controls__center">
<div class="center-buttons">
<div class="center-buttons__button -select" id="button-select">
<div class="center-buttons__label">RShift</div>
</div>
<div class="center-buttons__button -start" id="button-start">
<div class="center-buttons__label">Enter</div>
</div>
</div>
</div>
<div class="controls__ab">
<div class="ab">
<div class="ab__border">
<div class="ab__button -b" id="button-b">J</div>
</div>
<div class="ab__border">
<div class="ab__button -a" id="button-a">K</div>
</div>
</div>
</div>
</div>
</div>
</div>

<div class="source-link">
<a href="https://github.com/maxpoletaev/dendy" target="_blank">https://github.com/maxpoletaev/dendy</a>
</div>
Expand Down
53 changes: 39 additions & 14 deletions web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ Promise.all([wasmReady, documentReady]).then(async () => {
const TARGET_FPS = 60;
const SCALE = 2;

const audioBufferSize = go.AudioBufferSize;
const audioSampleRate = go.AudioSampleRate;

// ========================
// Canvas setup
// ========================
Expand All @@ -38,10 +35,10 @@ Promise.all([wasmReady, documentReady]).then(async () => {
// Audio setup
// ========================

const audioBufferSize = go.AudioBufferSize;
const audioSampleRate = go.AudioSampleRate;
console.log(`[INFO] audio sample rate: ${audioSampleRate}, buffer size: ${audioBufferSize}`);
let audioCtx = new AudioContext({
sampleRate: audioSampleRate,
});
let audioCtx = new AudioContext({sampleRate: audioSampleRate});

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


// ========================
// Input handling
// ========================
Expand Down Expand Up @@ -104,6 +100,25 @@ Promise.all([wasmReady, documentReady]).then(async () => {
}
});

const elementKeyMap = {
"dpad-up": BUTTON_UP,
"dpad-down": BUTTON_DOWN,
"dpad-left": BUTTON_LEFT,
"dpad-right": BUTTON_RIGHT,
"button-start": BUTTON_START,
"button-select": BUTTON_SELECT,
"button-b": BUTTON_B,
"button-a": BUTTON_A,
};

for (let [id, mask] of Object.entries(elementKeyMap)) {
let element = document.getElementById(id);
element.addEventListener("mousedown", () => { buttonsPressed |= mask; });
element.addEventListener("touchstart", () => { buttonsPressed |= mask; });
element.addEventListener("mouseup", () => { buttonsPressed &= ~mask; });
element.addEventListener("touchend", () => { buttonsPressed &= ~mask; });
}

// ========================
// ROM loading
// ========================
Expand Down Expand Up @@ -132,6 +147,16 @@ Promise.all([wasmReady, documentReady]).then(async () => {
});
}

document.addEventListener('dragover', (e) => {
e.preventDefault();
});

document.addEventListener('drop', (e) => {
e.preventDefault();
fileInput.files = e.dataTransfer.files;
fileInput.dispatchEvent(new Event('input'));
});

// ========================
// Game loop
// ========================
Expand All @@ -152,7 +177,7 @@ Promise.all([wasmReady, documentReady]).then(async () => {
let framePtr = go.GetFrameBufferPtr();
let image = new ImageData(new Uint8ClampedArray(getMemoryBuffer(), framePtr, WIDTH * HEIGHT * 4), WIDTH, HEIGHT);
ctx.putImageData(image, 0, 0);
return
return;
}

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

function loop() {
requestAnimationFrame(loop)
requestAnimationFrame(loop);

const now = performance.now()
const elapsed = now - lastFrameTime
if (elapsed < frameTime) return
const now = performance.now();
const elapsed = now - lastFrameTime;
if (elapsed < frameTime) return;

const excessTime = elapsed % frameTime
lastFrameTime = now - excessTime
const excessTime = elapsed % frameTime;
lastFrameTime = now - excessTime;

if (isInFocus()) {
executeFrame();
Expand Down
Loading

0 comments on commit 324e16d

Please sign in to comment.