@tmustier/pi-nes 0.2.3 → 0.2.5

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.
Files changed (37) hide show
  1. package/AGENTS.md +89 -1
  2. package/README.md +78 -49
  3. package/extensions/nes/config.ts +1 -18
  4. package/extensions/nes/index.ts +1 -21
  5. package/extensions/nes/native/nes-core/Cargo.lock +0 -2
  6. package/extensions/nes/native/nes-core/Cargo.toml +1 -1
  7. package/extensions/nes/native/nes-core/index.d.ts +5 -0
  8. package/extensions/nes/native/nes-core/index.node +0 -0
  9. package/extensions/nes/native/nes-core/native.d.ts +5 -0
  10. package/extensions/nes/native/nes-core/src/lib.rs +25 -0
  11. package/extensions/nes/native/nes-core/vendor/nes_rust/.cargo-ok +1 -0
  12. package/extensions/nes/native/nes-core/vendor/nes_rust/.cargo_vcs_info.json +5 -0
  13. package/extensions/nes/native/nes-core/vendor/nes_rust/.travis.yml +3 -0
  14. package/extensions/nes/native/nes-core/vendor/nes_rust/Cargo.toml +23 -0
  15. package/extensions/nes/native/nes-core/vendor/nes_rust/LICENSE +21 -0
  16. package/extensions/nes/native/nes-core/vendor/nes_rust/README.md +59 -0
  17. package/extensions/nes/native/nes-core/vendor/nes_rust/src/apu.rs +1114 -0
  18. package/extensions/nes/native/nes-core/vendor/nes_rust/src/audio.rs +6 -0
  19. package/extensions/nes/native/nes-core/vendor/nes_rust/src/button.rs +23 -0
  20. package/extensions/nes/native/nes-core/vendor/nes_rust/src/cpu.rs +2364 -0
  21. package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_audio.rs +49 -0
  22. package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_display.rs +47 -0
  23. package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_input.rs +33 -0
  24. package/extensions/nes/native/nes-core/vendor/nes_rust/src/display.rs +10 -0
  25. package/extensions/nes/native/nes-core/vendor/nes_rust/src/input.rs +7 -0
  26. package/extensions/nes/native/nes-core/vendor/nes_rust/src/joypad.rs +86 -0
  27. package/extensions/nes/native/nes-core/vendor/nes_rust/src/lib.rs +168 -0
  28. package/extensions/nes/native/nes-core/vendor/nes_rust/src/mapper.rs +502 -0
  29. package/extensions/nes/native/nes-core/vendor/nes_rust/src/memory.rs +73 -0
  30. package/extensions/nes/native/nes-core/vendor/nes_rust/src/ppu.rs +1378 -0
  31. package/extensions/nes/native/nes-core/vendor/nes_rust/src/register.rs +560 -0
  32. package/extensions/nes/native/nes-core/vendor/nes_rust/src/rom.rs +231 -0
  33. package/extensions/nes/nes-component.ts +3 -8
  34. package/extensions/nes/nes-core.ts +29 -9
  35. package/extensions/nes/paths.ts +40 -0
  36. package/extensions/nes/renderer.ts +53 -88
  37. package/package.json +1 -1
@@ -0,0 +1,231 @@
1
+ use memory::Memory;
2
+ use mapper::{Mapper, MapperFactory};
3
+
4
+ pub struct Rom {
5
+ header: RomHeader,
6
+ memory: Memory,
7
+ mapper: Box<dyn Mapper>
8
+ }
9
+
10
+ pub static HEADER_SIZE: usize = 16;
11
+
12
+ pub enum Mirrorings {
13
+ SingleScreen,
14
+ Horizontal,
15
+ Vertical,
16
+ FourScreen
17
+ }
18
+
19
+ impl Rom {
20
+ pub fn new(data: Vec<u8>) -> Self {
21
+ let header = RomHeader::new(data[0..HEADER_SIZE].to_vec());
22
+ let mapper = MapperFactory::create(&header);
23
+ Rom {
24
+ header: header,
25
+ memory: Memory::new(data[HEADER_SIZE..].to_vec()),
26
+ mapper: mapper
27
+ }
28
+ }
29
+
30
+ /**
31
+ * CPU memory address:
32
+ * 0x0000 - 0x1FFF: Character ROM access
33
+ * 0x8000 - 0xFFFF: Program ROM access
34
+ *
35
+ * To access wide range ROM data with limited CPU memory address space
36
+ * Mapper maps CPU memory address to ROM's.
37
+ * In general writing control registers in Mapper via .store() switches bank.
38
+ */
39
+ pub fn load(&self, address: u32) -> u8 {
40
+ let mut address_in_rom = 0 as u32;
41
+ if address < 0x2000 {
42
+ // load from character rom
43
+ address_in_rom += self.header.prg_rom_bank_num() as u32 * 0x4000;
44
+ address_in_rom += self.mapper.map_for_chr_rom(address);
45
+ } else {
46
+ address_in_rom += self.mapper.map(address);
47
+ }
48
+ self.memory.load(address_in_rom)
49
+ }
50
+
51
+ pub fn load_without_mapping(&self, address: u32) -> u8 {
52
+ self.memory.load(address)
53
+ }
54
+
55
+ /**
56
+ * In general writing with ROM address space updates control registers in Mapper.
57
+ */
58
+ pub fn store(&mut self, address: u32, value: u8) {
59
+ self.mapper.store(address, value);
60
+ }
61
+
62
+ pub fn valid(&self) -> bool {
63
+ self.header.is_nes()
64
+ }
65
+
66
+ pub fn has_chr_rom(&self) -> bool {
67
+ self.header.has_chr_rom()
68
+ }
69
+
70
+ pub fn has_battery_backed_ram(&self) -> bool {
71
+ self.header.has_battery_backed_ram()
72
+ }
73
+
74
+ pub fn mirroring_type(&self) -> Mirrorings {
75
+ match self.mapper.has_mirroring_type() {
76
+ true => self.mapper.mirroring_type(),
77
+ false => self.header.mirroring_type()
78
+ }
79
+ }
80
+
81
+ // @TODO: MMC3Mapper specific. Should this method be here?
82
+ pub fn irq_interrupted(&mut self) -> bool {
83
+ self.mapper.drive_irq_counter()
84
+ }
85
+ }
86
+
87
+ // @TODO: Cache
88
+ pub struct RomHeader {
89
+ data: Vec<u8>
90
+ }
91
+
92
+ impl RomHeader {
93
+ fn new(vec: Vec<u8>) -> Self {
94
+ let mut header = RomHeader {
95
+ data: Vec::new()
96
+ };
97
+ for i in 0..HEADER_SIZE {
98
+ header.data.push(vec[i]);
99
+ }
100
+ header
101
+ }
102
+
103
+ fn load(&self, address: u32) -> u8 {
104
+ self.data[address as usize]
105
+ }
106
+
107
+ fn is_nes(&self) -> bool {
108
+ if self.signature() == "NES" && self.magic_number() == 0x1a {
109
+ return true;
110
+ }
111
+ false
112
+ }
113
+
114
+ fn signature(&self) -> String {
115
+ let mut vec = Vec::new();
116
+ for i in 0..3 as u32 {
117
+ vec.push(self.load(i));
118
+ }
119
+ String::from_utf8(vec).unwrap()
120
+ }
121
+
122
+ fn magic_number(&self) -> u8 {
123
+ self.load(3)
124
+ }
125
+
126
+ pub fn prg_rom_bank_num(&self) -> u8 {
127
+ self.load(4)
128
+ }
129
+
130
+ pub fn chr_rom_bank_num(&self) -> u8 {
131
+ self.load(5)
132
+ }
133
+
134
+ fn has_chr_rom(&self) -> bool {
135
+ self.chr_rom_bank_num() > 0
136
+ }
137
+
138
+ fn control_byte1(&self) -> u8 {
139
+ self.load(6)
140
+ }
141
+
142
+ fn control_byte2(&self) -> u8 {
143
+ self.load(7)
144
+ }
145
+
146
+ fn _ram_bank_num(&self) -> u8 {
147
+ self.load(8)
148
+ }
149
+
150
+ fn _unused_field(&self) -> u64 {
151
+ let mut value = 0 as u64;
152
+ for i in 0..7 as u32 {
153
+ value = (value << 8) | self.load(9 + i) as u64;
154
+ }
155
+ value
156
+ }
157
+
158
+ fn extract_bits(&self, value: u8, offset: u8, size: u8) -> u8 {
159
+ (value >> offset) & ((1 << size) - 1)
160
+ }
161
+
162
+ fn mirroring_type(&self) -> Mirrorings {
163
+ match self.four_screen_mirroring() {
164
+ true => Mirrorings::FourScreen,
165
+ false => match self.extract_bits(self.control_byte1(), 0, 1) {
166
+ 0 => Mirrorings::Horizontal,
167
+ _ /* 1 */ => Mirrorings::Vertical
168
+ }
169
+ }
170
+ }
171
+
172
+ fn _is_horizontal_mirroring(&self) -> bool {
173
+ match self.mirroring_type() {
174
+ Mirrorings::Horizontal => true,
175
+ _ => false
176
+ }
177
+ }
178
+
179
+ pub fn has_battery_backed_ram(&self) -> bool {
180
+ self.extract_bits(self.control_byte1(), 1, 1) == 1
181
+ }
182
+
183
+ fn _trainer_512_bytes(&self) -> u8 {
184
+ self.extract_bits(self.control_byte1(), 2, 1)
185
+ }
186
+
187
+ fn four_screen_mirroring(&self) -> bool {
188
+ self.extract_bits(self.control_byte1(), 3, 1) == 1
189
+ }
190
+
191
+ pub fn mapper_num(&self) -> u8 {
192
+ let lower_bits = self.extract_bits(self.control_byte1(), 4, 4);
193
+ let higher_bits = self.extract_bits(self.control_byte2(), 4, 4);
194
+ (higher_bits << 4) | lower_bits
195
+ }
196
+ }
197
+
198
+ #[cfg(test)]
199
+ mod tests_rom {
200
+ use super::*;
201
+
202
+ #[test]
203
+ fn initialize() {
204
+ let r = Rom::new(vec![0; 17]);
205
+ }
206
+
207
+ #[test]
208
+ fn load() {
209
+ let r = Rom::new(vec![0; 17]);
210
+ assert_eq!(0, r.load(0));
211
+ }
212
+
213
+ #[test]
214
+ fn store() {
215
+ let mut r = Rom::new(vec![0; 17]);
216
+ r.store(0, 0);
217
+ }
218
+
219
+ #[test]
220
+ fn valid() {
221
+ let r = Rom::new(vec![0; 64]);
222
+ assert_eq!(false, r.valid());
223
+ let mut v = vec![0; 64];
224
+ v[0] = 0x4e; // N
225
+ v[1] = 0x45; // E
226
+ v[2] = 0x53; // S
227
+ v[3] = 0x1a; // magic number
228
+ let r2 = Rom::new(v);
229
+ assert_eq!(true, r2.valid());
230
+ }
231
+ }
@@ -11,14 +11,9 @@ const FRAME_WIDTH = 256;
11
11
  const FRAME_HEIGHT = 240;
12
12
 
13
13
  function readRgb(frameBuffer: FrameBuffer, index: number): [number, number, number] {
14
- if (frameBuffer.format === "rgb") {
15
- const data = frameBuffer.data as ReadonlyArray<number>;
16
- const base = index * 3;
17
- return [data[base] ?? 0, data[base + 1] ?? 0, data[base + 2] ?? 0];
18
- }
19
- const data = frameBuffer.data as ReadonlyArray<number>;
20
- const color = data[index] ?? 0;
21
- return [color & 0xff, (color >> 8) & 0xff, (color >> 16) & 0xff];
14
+ const data = frameBuffer.data;
15
+ const base = index * 3;
16
+ return [data[base] ?? 0, data[base + 1] ?? 0, data[base + 2] ?? 0];
22
17
  }
23
18
 
24
19
  function renderHalfBlock(
@@ -4,11 +4,8 @@ const require = createRequire(import.meta.url);
4
4
 
5
5
  export type NesButton = "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select";
6
6
 
7
- export type FrameBufferFormat = "packed" | "rgb";
8
-
9
7
  export interface FrameBuffer {
10
- format: FrameBufferFormat;
11
- data: ReadonlyArray<number> | Uint8Array;
8
+ data: Uint8Array;
12
9
  }
13
10
 
14
11
  export interface NesCore {
@@ -37,6 +34,11 @@ interface NativeNesInstance {
37
34
  reset(): void;
38
35
  pressButton(button: number): void;
39
36
  releaseButton(button: number): void;
37
+ hasBatteryBackedRam(): boolean;
38
+ getSram(): Uint8Array;
39
+ setSram(data: Uint8Array): void;
40
+ isSramDirty(): boolean;
41
+ markSramSaved(): void;
40
42
  getFramebuffer(): Uint8Array;
41
43
  }
42
44
 
@@ -76,6 +78,7 @@ class NativeNesCore implements NesCore {
76
78
  private readonly nes: NativeNesInstance;
77
79
  private readonly audioWarning: string | null;
78
80
  private readonly frameBuffer: Uint8Array;
81
+ private hasSram = false;
79
82
 
80
83
  constructor(enableAudio: boolean) {
81
84
  this.audioWarning = enableAudio
@@ -91,6 +94,7 @@ class NativeNesCore implements NesCore {
91
94
 
92
95
  loadRom(rom: Uint8Array): void {
93
96
  this.nes.setRom(rom);
97
+ this.hasSram = this.nes.hasBatteryBackedRam();
94
98
  this.nes.bootup();
95
99
  }
96
100
 
@@ -100,7 +104,7 @@ class NativeNesCore implements NesCore {
100
104
  }
101
105
 
102
106
  getFrameBuffer(): FrameBuffer {
103
- return { format: "rgb", data: this.frameBuffer };
107
+ return { data: this.frameBuffer };
104
108
  }
105
109
 
106
110
  setButton(button: NesButton, pressed: boolean): void {
@@ -113,16 +117,32 @@ class NativeNesCore implements NesCore {
113
117
  }
114
118
 
115
119
  getSram(): Uint8Array | null {
116
- return null;
120
+ if (!this.hasSram) {
121
+ return null;
122
+ }
123
+ return this.nes.getSram();
117
124
  }
118
125
 
119
- setSram(_sram: Uint8Array): void {}
126
+ setSram(sram: Uint8Array): void {
127
+ if (!this.hasSram) {
128
+ return;
129
+ }
130
+ this.nes.setSram(sram);
131
+ }
120
132
 
121
133
  isSramDirty(): boolean {
122
- return false;
134
+ if (!this.hasSram) {
135
+ return false;
136
+ }
137
+ return this.nes.isSramDirty();
123
138
  }
124
139
 
125
- markSramSaved(): void {}
140
+ markSramSaved(): void {
141
+ if (!this.hasSram) {
142
+ return;
143
+ }
144
+ this.nes.markSramSaved();
145
+ }
126
146
 
127
147
  getAudioWarning(): string | null {
128
148
  return this.audioWarning;
@@ -0,0 +1,40 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export function displayPath(value: string): string {
5
+ const home = os.homedir();
6
+ if (value.startsWith(home)) {
7
+ return `~${value.slice(home.length)}`;
8
+ }
9
+ return value;
10
+ }
11
+
12
+ export function expandHomePath(value: string): string {
13
+ if (value === "~") {
14
+ return os.homedir();
15
+ }
16
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
17
+ return path.join(os.homedir(), value.slice(2));
18
+ }
19
+ return value;
20
+ }
21
+
22
+ export function normalizePath(value: string, fallback: string): string {
23
+ const trimmed = value.trim();
24
+ if (!trimmed) {
25
+ return fallback;
26
+ }
27
+ return expandHomePath(trimmed);
28
+ }
29
+
30
+ export function resolvePathInput(input: string, cwd: string): string {
31
+ const trimmed = input.trim();
32
+ if (!trimmed) {
33
+ return cwd;
34
+ }
35
+ const expanded = expandHomePath(trimmed);
36
+ if (path.isAbsolute(expanded)) {
37
+ return expanded;
38
+ }
39
+ return path.resolve(cwd, expanded);
40
+ }
@@ -131,21 +131,8 @@ export class NesImageRenderer {
131
131
  return null;
132
132
  }
133
133
 
134
- const availableRows = getAvailableRows(tui, footerRows);
135
- const maxRows = getMaxImageRows(tui, footerRows);
136
- const cell = getCellDimensions();
137
- const maxWidthByRows = Math.floor(
138
- (maxRows * cell.heightPx * FRAME_WIDTH) / (FRAME_HEIGHT * cell.widthPx),
139
- );
140
- const maxWidthCells = Math.max(1, Math.min(widthCells, maxWidthByRows));
141
- const maxWidthPx = Math.max(1, maxWidthCells * cell.widthPx);
142
- const maxHeightPx = Math.max(1, maxRows * cell.heightPx);
143
- const maxScale = Math.min(maxWidthPx / FRAME_WIDTH, maxHeightPx / FRAME_HEIGHT);
144
- const requestedScale = Math.max(0.5, pixelScale) * maxScale;
145
- const scale = Math.min(maxScale, requestedScale);
146
- const columns = Math.max(1, Math.min(maxWidthCells, Math.floor((FRAME_WIDTH * scale) / cell.widthPx)));
147
- const rows = Math.max(1, Math.min(maxRows, Math.floor((FRAME_HEIGHT * scale) / cell.heightPx)));
148
- const padLeft = getHorizontalPadding(widthCells, columns);
134
+ const layout = computeKittyLayout(tui, widthCells, footerRows, pixelScale);
135
+ const { availableRows, columns, rows, padLeft } = layout;
149
136
 
150
137
  this.fillRawBufferTarget(frameBuffer, shared.buffer);
151
138
  const base64Name = Buffer.from(shared.name).toString("base64");
@@ -178,21 +165,8 @@ export class NesImageRenderer {
178
165
  footerRows: number,
179
166
  pixelScale: number,
180
167
  ): string[] {
181
- const availableRows = getAvailableRows(tui, footerRows);
182
- const maxRows = getMaxImageRows(tui, footerRows);
183
- const cell = getCellDimensions();
184
- const maxWidthByRows = Math.floor(
185
- (maxRows * cell.heightPx * FRAME_WIDTH) / (FRAME_HEIGHT * cell.widthPx),
186
- );
187
- const maxWidthCells = Math.max(1, Math.min(widthCells, maxWidthByRows));
188
- const maxWidthPx = Math.max(1, maxWidthCells * cell.widthPx);
189
- const maxHeightPx = Math.max(1, maxRows * cell.heightPx);
190
- const maxScale = Math.min(maxWidthPx / FRAME_WIDTH, maxHeightPx / FRAME_HEIGHT);
191
- const requestedScale = Math.max(0.5, pixelScale) * maxScale;
192
- const scale = Math.min(maxScale, requestedScale);
193
- const columns = Math.max(1, Math.min(maxWidthCells, Math.floor((FRAME_WIDTH * scale) / cell.widthPx)));
194
- const rows = Math.max(1, Math.min(maxRows, Math.floor((FRAME_HEIGHT * scale) / cell.heightPx)));
195
- const padLeft = getHorizontalPadding(widthCells, columns);
168
+ const layout = computeKittyLayout(tui, widthCells, footerRows, pixelScale);
169
+ const { availableRows, columns, rows, padLeft } = layout;
196
170
 
197
171
  this.fillRawBuffer(frameBuffer);
198
172
  const fd = this.ensureRawFile();
@@ -235,23 +209,8 @@ export class NesImageRenderer {
235
209
  footerRows: number,
236
210
  pixelScale: number,
237
211
  ): string[] {
238
- const availableRows = getAvailableRows(tui, footerRows);
239
- const maxRows = getMaxImageRows(tui, footerRows);
240
- const cell = getCellDimensions();
241
- const maxWidthByRows = Math.floor(
242
- (maxRows * cell.heightPx * FRAME_WIDTH) / (FRAME_HEIGHT * cell.widthPx),
243
- );
244
- const maxWidthCells = Math.max(1, Math.min(widthCells, maxWidthByRows));
245
- const maxWidthPx = Math.max(1, maxWidthCells * cell.widthPx);
246
- const maxHeightPx = Math.max(1, maxRows * cell.heightPx);
247
- const scale = Math.min(
248
- maxWidthPx / FRAME_WIDTH,
249
- maxHeightPx / FRAME_HEIGHT,
250
- ) * pixelScale;
251
- const targetWidth = Math.max(1, Math.floor(FRAME_WIDTH * scale));
252
- const targetHeight = Math.max(1, Math.floor(FRAME_HEIGHT * scale));
253
- const columns = Math.max(1, Math.min(maxWidthCells, Math.floor(targetWidth / cell.widthPx)));
254
- const padLeft = getHorizontalPadding(widthCells, columns);
212
+ const layout = computePngLayout(tui, widthCells, footerRows, pixelScale);
213
+ const { availableRows, maxWidthCells, padLeft, targetHeight, targetWidth } = layout;
255
214
 
256
215
  const hash = hashFrame(frameBuffer, targetWidth, targetHeight);
257
216
  if (!this.cachedImage || this.lastFrameHash !== hash) {
@@ -293,28 +252,13 @@ export class NesImageRenderer {
293
252
  }
294
253
 
295
254
  private fillRawBufferTarget(frameBuffer: FrameBuffer, target: Uint8Array): void {
296
- if (frameBuffer.format === "rgb") {
297
- const source = frameBuffer.data as ReadonlyArray<number>;
298
- if (source instanceof Uint8Array) {
299
- target.set(source.subarray(0, RAW_FRAME_BYTES));
300
- return;
301
- }
302
- const max = Math.min(source.length, RAW_FRAME_BYTES);
303
- for (let i = 0; i < max; i += 1) {
304
- target[i] = source[i] ?? 0;
305
- }
255
+ const source = frameBuffer.data;
256
+ if (source.length >= RAW_FRAME_BYTES) {
257
+ target.set(source.subarray(0, RAW_FRAME_BYTES));
306
258
  return;
307
259
  }
308
-
309
- let offset = 0;
310
- const source = frameBuffer.data as ReadonlyArray<number>;
311
- for (let i = 0; i < FRAME_WIDTH * FRAME_HEIGHT; i += 1) {
312
- const color = source[i] ?? 0;
313
- target[offset] = color & 0xff;
314
- target[offset + 1] = (color >> 8) & 0xff;
315
- target[offset + 2] = (color >> 16) & 0xff;
316
- offset += 3;
317
- }
260
+ target.set(source);
261
+ target.fill(0, source.length, RAW_FRAME_BYTES);
318
262
  }
319
263
 
320
264
  private getSharedMemoryModule(): KittyShmModule | null {
@@ -459,6 +403,40 @@ function getMaxImageRows(tui: TUI, footerRows: number): number {
459
403
  return Math.max(1, Math.floor(availableRows * IMAGE_HEIGHT_RATIO));
460
404
  }
461
405
 
406
+ function computeLayoutBase(tui: TUI, widthCells: number, footerRows: number) {
407
+ const availableRows = getAvailableRows(tui, footerRows);
408
+ const maxRows = getMaxImageRows(tui, footerRows);
409
+ const cell = getCellDimensions();
410
+ const maxWidthByRows = Math.floor(
411
+ (maxRows * cell.heightPx * FRAME_WIDTH) / (FRAME_HEIGHT * cell.widthPx),
412
+ );
413
+ const maxWidthCells = Math.max(1, Math.min(widthCells, maxWidthByRows));
414
+ const maxWidthPx = Math.max(1, maxWidthCells * cell.widthPx);
415
+ const maxHeightPx = Math.max(1, maxRows * cell.heightPx);
416
+ return { availableRows, maxRows, cell, maxWidthCells, maxWidthPx, maxHeightPx };
417
+ }
418
+
419
+ function computeKittyLayout(tui: TUI, widthCells: number, footerRows: number, pixelScale: number) {
420
+ const base = computeLayoutBase(tui, widthCells, footerRows);
421
+ const maxScale = Math.min(base.maxWidthPx / FRAME_WIDTH, base.maxHeightPx / FRAME_HEIGHT);
422
+ const requestedScale = Math.max(0.5, pixelScale) * maxScale;
423
+ const scale = Math.min(maxScale, requestedScale);
424
+ const columns = Math.max(1, Math.min(base.maxWidthCells, Math.floor((FRAME_WIDTH * scale) / base.cell.widthPx)));
425
+ const rows = Math.max(1, Math.min(base.maxRows, Math.floor((FRAME_HEIGHT * scale) / base.cell.heightPx)));
426
+ const padLeft = getHorizontalPadding(widthCells, columns);
427
+ return { ...base, columns, rows, padLeft };
428
+ }
429
+
430
+ function computePngLayout(tui: TUI, widthCells: number, footerRows: number, pixelScale: number) {
431
+ const base = computeLayoutBase(tui, widthCells, footerRows);
432
+ const scale = Math.min(base.maxWidthPx / FRAME_WIDTH, base.maxHeightPx / FRAME_HEIGHT) * pixelScale;
433
+ const targetWidth = Math.max(1, Math.floor(FRAME_WIDTH * scale));
434
+ const targetHeight = Math.max(1, Math.floor(FRAME_HEIGHT * scale));
435
+ const columns = Math.max(1, Math.min(base.maxWidthCells, Math.floor(targetWidth / base.cell.widthPx)));
436
+ const padLeft = getHorizontalPadding(widthCells, columns);
437
+ return { ...base, targetWidth, targetHeight, padLeft };
438
+ }
439
+
462
440
  function centerLines(lines: string[], totalRows: number): string[] {
463
441
  if (lines.length >= totalRows) {
464
442
  return lines;
@@ -495,37 +473,24 @@ function insertPaddingAfterMoveUp(line: string, padLeft: number): string {
495
473
  }
496
474
 
497
475
  function readRgb(frameBuffer: FrameBuffer, index: number): [number, number, number] {
498
- if (frameBuffer.format === "rgb") {
499
- const data = frameBuffer.data as ReadonlyArray<number>;
500
- const base = index * 3;
501
- return [data[base] ?? 0, data[base + 1] ?? 0, data[base + 2] ?? 0];
502
- }
503
- const data = frameBuffer.data as ReadonlyArray<number>;
504
- const color = data[index] ?? 0;
505
- return [color & 0xff, (color >> 8) & 0xff, (color >> 16) & 0xff];
506
- }
507
-
508
- function readPacked(frameBuffer: FrameBuffer, index: number): number {
509
- if (frameBuffer.format === "packed") {
510
- const data = frameBuffer.data as ReadonlyArray<number>;
511
- return data[index] ?? 0;
512
- }
513
- const data = frameBuffer.data as ReadonlyArray<number>;
476
+ const data = frameBuffer.data;
514
477
  const base = index * 3;
515
- const r = data[base] ?? 0;
516
- const g = data[base + 1] ?? 0;
517
- const b = data[base + 2] ?? 0;
518
- return r | (g << 8) | (b << 16);
478
+ return [data[base] ?? 0, data[base + 1] ?? 0, data[base + 2] ?? 0];
519
479
  }
520
480
 
521
481
  function hashFrame(frameBuffer: FrameBuffer, width: number, height: number): number {
522
482
  let hash = width ^ (height << 16);
523
483
  const stepX = Math.max(1, Math.floor(FRAME_WIDTH / 64));
524
484
  const stepY = Math.max(1, Math.floor(FRAME_HEIGHT / 64));
485
+ const data = frameBuffer.data;
525
486
  for (let y = 0; y < FRAME_HEIGHT; y += stepY) {
526
487
  const rowOffset = y * FRAME_WIDTH;
527
488
  for (let x = 0; x < FRAME_WIDTH; x += stepX) {
528
- const color = readPacked(frameBuffer, rowOffset + x);
489
+ const base = (rowOffset + x) * 3;
490
+ const r = data[base] ?? 0;
491
+ const g = data[base + 1] ?? 0;
492
+ const b = data[base + 2] ?? 0;
493
+ const color = r | (g << 8) | (b << 16);
529
494
  hash = ((hash << 5) - hash + color) | 0;
530
495
  }
531
496
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-nes",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "NES emulator extension for pi",
5
5
  "keywords": [
6
6
  "pi-package",