Search › A-Z › N › Nintendo DS
Nintendo DS
This is the Nintendo DS running in your browser: the DeSmuME core compiled to WebAssembly with Emscripten and self-hosted, no CDN. It boots an openly-licensed homebrew .nds under an HLE BIOS (no firmware files needed), renders both DS screens stacked with the lower screen as a live touchscreen, and plugs into the shared in-frame debugger for the ARM946E-S (ARM9) main CPU and the ARM7TDMI co-processor.
Runs on: Web browser
Nintendo DS Online Emulator
Play Nintendo DS using JavaScript directly in your browser.
Controls
Configurations
| Configuration | Emulator | Machine | OS | Legal | |
|---|---|---|---|---|---|
| DS Homebrew | Nintendo DS | Nintendo DS | open | Open ⛶ |
Machines emulated
Chips
Notes
Embedding
The Nintendo DS core is DeSmuME (the TASEmulators DS emulator), compiled to WebAssembly with Emscripten. DeSmuME normally ships a Qt/Cocoa/SDL desktop app; rather than pull in any of that we wrote a tiny standalone Emscripten frontend, nds_wasm.cpp, that talks to DeSmuME's OWN core API (NDS_Init / NDS_LoadROM / NDS_Reset / NDS_exec) with the software rasterizer and a dummy sound core — no SDL, no OpenGL, no libretro — and exposes it to JavaScript through a handful of flat functions. The module is built with -sMODULARIZE=1 so nds.js is a factory you instantiate. We self-host everything (no CDN): the rebuilt nds.js / nds.wasm and each homebrew .nds.
// nds.js defines a global Module factory; instantiate it, stage the ROM, boot.
var ds = await Module({ locateFile: f => SRC + f }); // finds nds.wasm
ds._wasm_boot(); // NDS_Init + soft rasterizer + HLE BIOS
var p = ds._wasm_rom_buf(rom.length); ds.HEAPU8.set(rom, p); // stage the .nds
ds._wasm_load(p, rom.length); // write to MEMFS, NDS_LoadROM, direct-boot
Rendering is manual and DUAL-SCREEN. DeSmuME rasters two 256×192 screens into one 16-bit BGR555 buffer; the frontend converts the stacked 256×384 picture to RGBA in the WASM heap. Each frame we blit that buffer to an off-screen canvas and draw it to the visible canvas — the top half is the main screen, the bottom half is the touchscreen:
var rgba = new Uint8ClampedArray(ds.HEAPU8.buffer, ds._wasm_fb_ptr(), 256*384*4);
img.data.set(rgba); bctx.putImageData(img, 0, 0);
ctx.drawImage(back, 0,0, canvas.width, canvas.height); // both screens, stacked
Input is the DS key latch plus the touchscreen. Keys are pushed as one bitmask (D-pad + A/B/X/Y + L/R + Start/Select) through _wasm_set_pad; the lower screen's pointer events map to touch coordinates and call _wasm_set_touch(x,y) / _wasm_release_touch(), which drive DeSmuME's NDS_setTouchPos / NDS_releaseTouch.
| Member | Kind | What it does |
|---|---|---|
_wasm_boot() | export | NDS_Init, software 3D rasterizer, dummy sound, HLE-BIOS direct-boot config. |
_wasm_rom_buf(n) / _wasm_load(p,n) | export | Allocate a staging pointer for the .nds; write it to MEMFS, NDS_LoadROM, NDS_Reset (direct-boots). |
_wasm_tick() | export | Run exactly one video frame (NDS_exec) and convert both screens to RGBA. Our frame-step and the loop's advance. |
_wasm_fb_ptr() | export | Pointer to the stacked 256×384 RGBA framebuffer in the heap. |
_wasm_set_pad(bits) | export | Write the DS key latch (D-pad + A/B/X/Y + L/R + Start/Select). |
_wasm_set_touch(x,y) / _wasm_release_touch() | export | Set / clear the touchscreen position on the lower display. |
_wasm_dbg_* | export | The debug sampling API over BOTH ARM cores (registers, memory, step, reset) — see below. |
Debugger integration
This is a WebAssembly core, yet it gets the same live debugger as the pure-JavaScript machines — real registers, real memory, a real single-instruction step, plus execution breakpoints and memory watchpoints. The DS has TWO ARM CPUs; DeSmuME keeps both in plain C structs (NDS_ARM9 and NDS_ARM7, of type armcpu_t), whose R[16] / CPSR / instruct_adr are directly reachable, and whose armcpu_exec<PROCNUM>() runs exactly one instruction. We expose these through a small sampling API from our standalone frontend.
1 · Exactly what was added. One new file, nds_wasm.cpp, is the whole frontend: it drives the core and appends a block of EMSCRIPTEN_KEEPALIVE functions that read live state off the two armcpu_t structs. They are sampling functions: the debugger calls them ~10×/second while its window is open, and once per Step. There is no per-instruction or per-cycle callback anywhere — the emulator hot path (NDS_exec) is byte-for-byte unchanged (the golden performance rule). A second tiny file, task_stub.cpp, replaces DeSmuME's pthread-based Task with a synchronous one so the build is single-threaded (no SharedArrayBuffer, hosts anywhere).
// nds_wasm.cpp — sampling only, no hot-path hook. proc: 0 = ARM9, 1 = ARM7.
static armcpu_t *cpu(int proc){ return proc ? &NDS_ARM7 : &NDS_ARM9; }
u32 wasm_dbg_pc(int p){ return cpu(p)->instruct_adr; }
u32 wasm_dbg_reg(int p,int i){ return cpu(p)->R[i&15]; }
u32 wasm_dbg_cpsr(int p){ return cpu(p)->CPSR.val; }
u32 wasm_dbg_read(int p,u32 a){ // side-effect-free (MMU_AT_DEBUG)
return p ? _MMU_read08<ARMCPU_ARM7,MMU_AT_DEBUG>(a)
: _MMU_read08<ARMCPU_ARM9,MMU_AT_DEBUG>(a); }
void wasm_dbg_step(int p){ p ? armcpu_exec<1>() : armcpu_exec<0>(); } // ONE instruction
2 · What is REAL here.
- Real registers, both cores — R0-R15 and CPSR (N Z C V + the T/mode bits) for the ARM9 (ARM946E-S) main CPU and the ARM7TDMI co-processor, read and written live off the two
armcpu_tstructs. The ARM9 is the primary; the ARM7's PC and registers appear as a secondary group so you can watch both. - Real single-instruction step —
Step icallswasm_dbg_step(0)=armcpu_exec<0>(), which executes exactly one ARM9 instruction; PC and the registers change by one instruction per click. - Side-effect-free memory — reads go through
_MMU_read08<…,MMU_AT_DEBUG>, the DS's debug access path that bypasses I/O side effects, plus a direct window on the 4 MB main RAM (MMU.MAIN_MEM), so auto-polling the hex/disasm view never acknowledges an interrupt or clears a latch. - Execution breakpoints & memory watchpoints — implemented host-side in the run loop (see below), so they cost nothing when the debugger is closed.
3 · The reused decoder. The ARM9 and ARM7 are both ARM cores, so the machine plug-in points its cpu.decoder at the shared arm7 ARM disassembler — no new decoder was written. (It decodes 32-bit ARM state; when a core is in Thumb state the disassembly is best-effort, which the Notes call out.)
4 · Breakpoints and watchpoints without a hot-path hook. When nothing is armed, the loop runs a whole frame at native speed with _wasm_tick() (NDS_exec). As soon as a breakpoint or watchpoint is set, the loop switches to stepping the ARM9 one instruction at a time with wasm_dbg_step(0), comparing the live PC against the breakpoint set and the watched addresses against their last sampled value, and pausing on a hit. This is a pure host-side check that only runs while a guard is armed. (While single-stepping the ARM9 the ARM7/video do not advance in lockstep, so the picture holds on its last frame until you resume.)
Architecture
The Nintendo DS (Nintendo, 2004) is a dual-screen handheld — the first mainstream console with a touchscreen — built around two ARM CPUs sharing 4 MB of main RAM. DeSmuME emulates the whole machine; our build is a thin standalone Emscripten port of its core:
- ARM946E-S (ARM9) — the ARMv5TE main processor at ~67 MHz, with instruction/data caches and TCM. This is the primary CPU the debugger samples;
armcpu_exec<0>()is the run-one-instruction primitive used for single-stepping. - ARM7TDMI (ARM7) — the ARMv4T co-processor at ~33 MHz, handling sound, Wi-Fi, the touchscreen/firmware SPI and general I/O. Exposed as a secondary core (its PC and registers) — the same ARM7TDMI as the Game Boy Advance.
- 2D + 3D video — two 2D engines (main + sub) each drive one 256×192 screen; a hardware 3D geometry + rasterizer pipeline feeds one of them. DeSmuME renders 3D with a software rasterizer in this build (no WebGL needed).
- Touchscreen — a resistive panel over the lower screen, read by the ARM7 through the TSC; pointer events on the lower half of the canvas set the touch position.
- Memory map — 4 MB main RAM at
0x02000000(shared by both CPUs), plus per-CPU TCM/WRAM, the I/O register block, palette/VRAM/OAM, and the cartridge. Homebrew.ndsimages direct-boot from the cartridge header under an HLE BIOS, so no external BIOS or firmware files are needed.
How to build this exact artefact. Toolchain: Homebrew emscripten (emcc on PATH).
git clone https://github.com/TASEmulators/desmume.git
# add nds_wasm.cpp (core-API frontend + wasm_dbg_* hooks) and task_stub.cpp,
# then compile the core (desmume/src/*.cpp, minus the SSE/AVX/NEON/GL/JIT/Lua
# variants) + the shim with the software rasterizer:
emcc -O2 -std=c++17 -sUSE_ZLIB=1 <core .cpp files> nds_wasm.cpp task_stub.cpp -o nds.js -sMODULARIZE=1 -sEXPORT_NAME=Module -sALLOW_MEMORY_GROWTH=1 -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8,HEAPU32 -sEXPORTED_FUNCTIONS=_wasm_boot,_wasm_load,_wasm_tick,_wasm_dbg_pc,... # -> nds.js + .wasm
The build is single-threaded (the pthread Task is stubbed synchronous), so it hosts anywhere with no SharedArrayBuffer / COOP-COEP headers. The interpreter cores run in plain C++ with no dynarec, so nothing needs writable-executable memory.