@tmustier/pi-nes 0.2.35 → 0.2.37
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/AGENTS.md +20 -12
- package/README.md +3 -3
- package/extensions/nes/index.ts +65 -40
- package/package.json +3 -2
package/AGENTS.md
CHANGED
|
@@ -20,11 +20,12 @@ pi-nes/
|
|
|
20
20
|
├── config.ts # User config (~/.pi/nes/config.json)
|
|
21
21
|
├── paths.ts # Path resolution utilities
|
|
22
22
|
├── roms.ts # ROM directory listing
|
|
23
|
+
├── rom-selector.ts # Filterable ROM picker UI
|
|
23
24
|
├── saves.ts # SRAM persistence
|
|
24
25
|
└── native/
|
|
25
26
|
├── nes-core/ # Rust NES emulator addon (required)
|
|
26
|
-
│ ├── Cargo.toml # Dependencies: vendored nes_rust, napi
|
|
27
|
-
│ ├── vendor/nes_rust/ # Patched nes_rust crate (SRAM helpers)
|
|
27
|
+
│ ├── Cargo.toml # Dependencies: vendored nes_rust, napi, optional cpal
|
|
28
|
+
│ ├── vendor/nes_rust/ # Patched nes_rust crate (SRAM helpers + mapper fixes)
|
|
28
29
|
│ ├── src/lib.rs # Exposes NativeNes class via napi-rs
|
|
29
30
|
│ └── index.node # Compiled binary
|
|
30
31
|
└── kitty-shm/ # Rust shared memory addon (optional)
|
|
@@ -34,7 +35,7 @@ pi-nes/
|
|
|
34
35
|
|
|
35
36
|
### Native Core
|
|
36
37
|
|
|
37
|
-
The emulator uses the [`nes_rust`](https://crates.io/crates/nes_rust) crate (vendored + patched in `native/nes-core/vendor/nes_rust`
|
|
38
|
+
The emulator uses the [`nes_rust`](https://crates.io/crates/nes_rust) crate (vendored + patched in `native/nes-core/vendor/nes_rust`) with [napi-rs](https://napi.rs) bindings. Optional audio uses a Rust-only backend (`cpal`) when built with `audio-cpal`.
|
|
38
39
|
|
|
39
40
|
### Vendored `nes_rust` workflow
|
|
40
41
|
|
|
@@ -46,12 +47,16 @@ The emulator uses the [`nes_rust`](https://crates.io/crates/nes_rust) crate (ven
|
|
|
46
47
|
**API exposed to JavaScript:**
|
|
47
48
|
- `new NativeNes()` - Create emulator instance
|
|
48
49
|
- `setRom(Uint8Array)` - Load ROM data
|
|
49
|
-
- `bootup()`
|
|
50
|
+
- `bootup()` - Start emulation
|
|
50
51
|
- `stepFrame()` - Advance one frame (~60fps)
|
|
52
|
+
- `refreshFramebuffer()` - Copy framebuffer from core
|
|
51
53
|
- `pressButton(n)` / `releaseButton(n)` - Controller input (0=select, 1=start, 2=A, 3=B, 4-7=dpad)
|
|
54
|
+
- `setVideoFilter(mode)` - 0=off, 1=ntsc-composite, 2=ntsc-svideo, 3=ntsc-rgb
|
|
55
|
+
- `setAudioEnabled(bool)` - Returns false if audio backend unavailable
|
|
52
56
|
- `hasBatteryBackedRam()` - Whether the ROM supports battery SRAM
|
|
53
57
|
- `getSram()` / `setSram(Uint8Array)` - Read/write SRAM
|
|
54
58
|
- `isSramDirty()` / `markSramSaved()` - Dirty tracking for SRAM persistence
|
|
59
|
+
- `getDebugState()` - CPU/mapper debug info
|
|
55
60
|
- `getFramebuffer()` - Returns RGB pixel data (256×240×3 bytes, zero-copy via external buffer)
|
|
56
61
|
|
|
57
62
|
### Rendering Pipeline
|
|
@@ -67,7 +72,7 @@ NES Core → RGB framebuffer (256×240×3) → Renderer → Terminal
|
|
|
67
72
|
|
|
68
73
|
- Image mode (`renderer: "image"`) runs at ~30fps to keep emulation stable
|
|
69
74
|
- Text mode (`renderer: "text"`) runs at ~60fps in an overlay
|
|
70
|
-
- Image mode uses
|
|
75
|
+
- Image mode uses a windowed overlay (90% width/height) to avoid Kitty full-screen artifacts
|
|
71
76
|
|
|
72
77
|
### Session Lifecycle
|
|
73
78
|
|
|
@@ -86,8 +91,11 @@ Requires Rust toolchain (cargo + rustc).
|
|
|
86
91
|
cd extensions/nes/native/nes-core
|
|
87
92
|
npm install && npm run build
|
|
88
93
|
|
|
94
|
+
# NES core with audio (optional)
|
|
95
|
+
npm run build:audio
|
|
96
|
+
|
|
89
97
|
# Kitty shared memory (optional, faster rendering)
|
|
90
|
-
cd
|
|
98
|
+
cd ../kitty-shm
|
|
91
99
|
npm install && npm run build
|
|
92
100
|
```
|
|
93
101
|
|
|
@@ -95,13 +103,13 @@ The addons compile to `index.node`. The JS wrapper (`index.js`) tries to load it
|
|
|
95
103
|
|
|
96
104
|
## Known Limitations
|
|
97
105
|
|
|
98
|
-
- **
|
|
99
|
-
- **
|
|
100
|
-
- **
|
|
106
|
+
- **Audio is opt-in** — Requires a native core built with `audio-cpal` and `enableAudio: true`.
|
|
107
|
+
- **Overlay flicker** — Kitty overlay can show a 1-line flicker at overlay boundaries (see issue #9).
|
|
108
|
+
- **No save states** — Only battery-backed SRAM saves are persisted.
|
|
101
109
|
|
|
102
110
|
## Release and Publishing
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
### Version Bumps (Git + npm)
|
|
105
113
|
|
|
106
114
|
Preferred flow (creates a git tag):
|
|
107
115
|
|
|
@@ -127,7 +135,7 @@ git push
|
|
|
127
135
|
git push --tags
|
|
128
136
|
```
|
|
129
137
|
|
|
130
|
-
|
|
138
|
+
### npm Publish
|
|
131
139
|
|
|
132
140
|
```bash
|
|
133
141
|
npm login
|
|
@@ -140,7 +148,7 @@ If you need to validate the tarball first:
|
|
|
140
148
|
npm pack
|
|
141
149
|
```
|
|
142
150
|
|
|
143
|
-
|
|
151
|
+
### GitHub Release Notes
|
|
144
152
|
|
|
145
153
|
```bash
|
|
146
154
|
gh release create vX.Y.Z --title "vX.Y.Z" --notes "<release notes>"
|
package/README.md
CHANGED
|
@@ -51,13 +51,13 @@ On first run, you'll be prompted to set your ROM directory and display quality.
|
|
|
51
51
|
|---------|-------------|
|
|
52
52
|
| `/nes` | Pick a ROM or reattach to running session |
|
|
53
53
|
| `/nes <path>` | Load a specific ROM file |
|
|
54
|
-
| `/nes config` |
|
|
55
|
-
| `/nes-config` | Toggle audio +
|
|
54
|
+
| `/nes config` | Quick setup (ROM directory + audio) |
|
|
55
|
+
| `/nes-config` | Toggle audio, quality, and display style + advanced options |
|
|
56
56
|
| `/nes debug` | Show FPS and memory stats |
|
|
57
57
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
60
|
-
Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup, or `/nes-config` to toggle audio inline and access advanced options.
|
|
60
|
+
Config is stored at `~/.pi/nes/config.json`. Use `/nes config` for quick setup (ROM dir + audio), or `/nes-config` to toggle audio/quality/style inline and access advanced options.
|
|
61
61
|
|
|
62
62
|
```json
|
|
63
63
|
{
|
package/extensions/nes/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
loadConfig,
|
|
15
15
|
normalizeConfig,
|
|
16
16
|
saveConfig,
|
|
17
|
+
type ImageQuality,
|
|
17
18
|
type NesConfig,
|
|
18
19
|
type VideoFilter,
|
|
19
20
|
} from "./config.js";
|
|
@@ -27,6 +28,17 @@ const IMAGE_RENDER_INTERVAL_BALANCED_MS = 1000 / 30;
|
|
|
27
28
|
const IMAGE_RENDER_INTERVAL_HIGH_MS = 1000 / 60;
|
|
28
29
|
const TEXT_RENDER_INTERVAL_MS = 1000 / 60;
|
|
29
30
|
|
|
31
|
+
const AUDIO_OPTIONS = ["off (default)", "on"] as const;
|
|
32
|
+
const QUALITY_OPTIONS: Array<{ label: string; value: ImageQuality }> = [
|
|
33
|
+
{ label: "Balanced (default) — 30 fps", value: "balanced" },
|
|
34
|
+
{ label: "High — 60 fps", value: "high" },
|
|
35
|
+
];
|
|
36
|
+
const DISPLAY_FILTER_OPTIONS: Array<{ label: string; value: VideoFilter }> = [
|
|
37
|
+
{ label: "CRT Classic (default) — authentic scanlines + color bleed", value: "ntsc-composite" },
|
|
38
|
+
{ label: "CRT Soft — subtle retro look", value: "ntsc-rgb" },
|
|
39
|
+
{ label: "Sharp — pixel-perfect, no filtering", value: "off" },
|
|
40
|
+
];
|
|
41
|
+
|
|
30
42
|
let activeSession: NesSession | null = null;
|
|
31
43
|
|
|
32
44
|
// ROM selection helpers.
|
|
@@ -163,31 +175,13 @@ async function configureWithWizard(
|
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
"High — 60 fps",
|
|
169
|
-
]);
|
|
170
|
-
if (!qualityChoice) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const isHighQuality = qualityChoice.startsWith("High");
|
|
175
|
-
const imageQuality = isHighQuality ? "high" : "balanced";
|
|
176
|
-
|
|
177
|
-
const filterOptions: Array<{ label: string; value: VideoFilter }> = [
|
|
178
|
-
{ label: "CRT Classic (default) — authentic scanlines + color bleed", value: "ntsc-composite" },
|
|
179
|
-
{ label: "CRT Soft — subtle retro look", value: "ntsc-rgb" },
|
|
180
|
-
{ label: "Sharp — pixel-perfect, no filtering", value: "off" },
|
|
181
|
-
];
|
|
182
|
-
const filterChoice = await ctx.ui.select(
|
|
183
|
-
"Display style",
|
|
184
|
-
filterOptions.map((option) => option.label),
|
|
185
|
-
);
|
|
186
|
-
if (!filterChoice) {
|
|
178
|
+
const audioChoice = await ctx.ui.select("Audio", ["Off (default)", "On"]);
|
|
179
|
+
if (!audioChoice) {
|
|
187
180
|
return false;
|
|
188
181
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
182
|
+
const enableAudio = audioChoice === "On";
|
|
183
|
+
const imageQuality = config.imageQuality;
|
|
184
|
+
const videoFilter = config.videoFilter;
|
|
191
185
|
const pixelScale = config.pixelScale;
|
|
192
186
|
|
|
193
187
|
const defaultSaveDir = getDefaultSaveDir(config.romDir);
|
|
@@ -197,35 +191,62 @@ async function configureWithWizard(
|
|
|
197
191
|
...config,
|
|
198
192
|
romDir,
|
|
199
193
|
saveDir,
|
|
194
|
+
enableAudio,
|
|
200
195
|
imageQuality,
|
|
201
196
|
videoFilter,
|
|
202
197
|
pixelScale,
|
|
203
198
|
});
|
|
204
199
|
await saveConfig(normalized);
|
|
205
200
|
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
201
|
+
await ctx.ui.select(
|
|
202
|
+
"Toggle audio, quality, and more in /nes-config",
|
|
203
|
+
["Run /nes to load your games"],
|
|
204
|
+
);
|
|
206
205
|
return true;
|
|
207
206
|
}
|
|
208
207
|
|
|
209
208
|
type ConfigMenuResult = "close" | "more";
|
|
210
209
|
|
|
210
|
+
type ConfigUpdate = Partial<NesConfig>;
|
|
211
|
+
|
|
211
212
|
class NesConfigMenu extends Container {
|
|
212
213
|
private readonly settingsList: SettingsList;
|
|
213
214
|
|
|
214
215
|
constructor(
|
|
215
216
|
config: NesConfig,
|
|
216
217
|
theme: Theme,
|
|
217
|
-
|
|
218
|
+
onUpdate: (update: ConfigUpdate) => void,
|
|
218
219
|
onDone: (result: ConfigMenuResult) => void,
|
|
219
220
|
) {
|
|
220
221
|
super();
|
|
221
222
|
const audioValue = config.enableAudio ? "on" : "off (default)";
|
|
223
|
+
const qualityValue =
|
|
224
|
+
QUALITY_OPTIONS.find((option) => option.value === config.imageQuality)?.label
|
|
225
|
+
?? QUALITY_OPTIONS[0].label;
|
|
226
|
+
const displayValue =
|
|
227
|
+
DISPLAY_FILTER_OPTIONS.find((option) => option.value === config.videoFilter)?.label
|
|
228
|
+
?? DISPLAY_FILTER_OPTIONS[0].label;
|
|
222
229
|
const items: SettingItem[] = [
|
|
223
230
|
{
|
|
224
231
|
id: "audio",
|
|
225
232
|
label: "Audio",
|
|
226
233
|
description: "Enable audio output (requires native core built with audio-cpal)",
|
|
227
234
|
currentValue: audioValue,
|
|
228
|
-
values: [
|
|
235
|
+
values: [...AUDIO_OPTIONS],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: "quality",
|
|
239
|
+
label: "Quality",
|
|
240
|
+
description: "Target frame rate for image rendering",
|
|
241
|
+
currentValue: qualityValue,
|
|
242
|
+
values: QUALITY_OPTIONS.map((option) => option.label),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "display",
|
|
246
|
+
label: "Display style",
|
|
247
|
+
description: "CRT filter preset (applied in the native core)",
|
|
248
|
+
currentValue: displayValue,
|
|
249
|
+
values: DISPLAY_FILTER_OPTIONS.map((option) => option.label),
|
|
229
250
|
},
|
|
230
251
|
{
|
|
231
252
|
id: "more",
|
|
@@ -238,11 +259,21 @@ class NesConfigMenu extends Container {
|
|
|
238
259
|
|
|
239
260
|
this.settingsList = new SettingsList(
|
|
240
261
|
items,
|
|
241
|
-
|
|
262
|
+
8,
|
|
242
263
|
getSettingsListTheme(),
|
|
243
264
|
(id, value) => {
|
|
244
265
|
if (id === "audio") {
|
|
245
|
-
|
|
266
|
+
onUpdate({ enableAudio: value === "on" });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (id === "quality") {
|
|
270
|
+
const option = QUALITY_OPTIONS.find((entry) => entry.label === value);
|
|
271
|
+
onUpdate({ imageQuality: option?.value ?? config.imageQuality });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (id === "display") {
|
|
275
|
+
const option = DISPLAY_FILTER_OPTIONS.find((entry) => entry.label === value);
|
|
276
|
+
onUpdate({ videoFilter: option?.value ?? config.videoFilter });
|
|
246
277
|
return;
|
|
247
278
|
}
|
|
248
279
|
if (id === "more") {
|
|
@@ -271,20 +302,14 @@ async function editConfig(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
271
302
|
return;
|
|
272
303
|
}
|
|
273
304
|
const config = await loadConfig();
|
|
305
|
+
const updateConfig = async (update: ConfigUpdate) => {
|
|
306
|
+
const normalized = normalizeConfig({ ...config, ...update });
|
|
307
|
+
await saveConfig(normalized);
|
|
308
|
+
Object.assign(config, normalized);
|
|
309
|
+
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
310
|
+
};
|
|
274
311
|
const action = await ctx.ui.custom<ConfigMenuResult>((_tui, theme, _keybindings, done) =>
|
|
275
|
-
new NesConfigMenu(
|
|
276
|
-
config,
|
|
277
|
-
theme,
|
|
278
|
-
(enabled) => {
|
|
279
|
-
void (async () => {
|
|
280
|
-
const normalized = normalizeConfig({ ...config, enableAudio: enabled });
|
|
281
|
-
await saveConfig(normalized);
|
|
282
|
-
config.enableAudio = normalized.enableAudio;
|
|
283
|
-
ctx.ui.notify(`Saved config to ${getConfigPath()}`, "info");
|
|
284
|
-
})();
|
|
285
|
-
},
|
|
286
|
-
done,
|
|
287
|
-
),
|
|
312
|
+
new NesConfigMenu(config, theme, (update) => void updateConfig(update), done),
|
|
288
313
|
);
|
|
289
314
|
if (action !== "more") {
|
|
290
315
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmustier/pi-nes",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"description": "NES emulator extension for pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"pi": {
|
|
34
34
|
"extensions": [
|
|
35
35
|
"./extensions/nes"
|
|
36
|
-
]
|
|
36
|
+
],
|
|
37
|
+
"image": "https://raw.githubusercontent.com/tmustier/pi-nes/main/assets/demo.gif"
|
|
37
38
|
}
|
|
38
39
|
}
|