@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.
- package/AGENTS.md +89 -1
- package/README.md +78 -49
- package/extensions/nes/config.ts +1 -18
- package/extensions/nes/index.ts +1 -21
- package/extensions/nes/native/nes-core/Cargo.lock +0 -2
- package/extensions/nes/native/nes-core/Cargo.toml +1 -1
- package/extensions/nes/native/nes-core/index.d.ts +5 -0
- package/extensions/nes/native/nes-core/index.node +0 -0
- package/extensions/nes/native/nes-core/native.d.ts +5 -0
- package/extensions/nes/native/nes-core/src/lib.rs +25 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/.cargo-ok +1 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/.cargo_vcs_info.json +5 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/.travis.yml +3 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/Cargo.toml +23 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/LICENSE +21 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/README.md +59 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/apu.rs +1114 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/audio.rs +6 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/button.rs +23 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/cpu.rs +2364 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_audio.rs +49 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_display.rs +47 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/default_input.rs +33 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/display.rs +10 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/input.rs +7 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/joypad.rs +86 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/lib.rs +168 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/mapper.rs +502 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/memory.rs +73 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/ppu.rs +1378 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/register.rs +560 -0
- package/extensions/nes/native/nes-core/vendor/nes_rust/src/rom.rs +231 -0
- package/extensions/nes/nes-component.ts +3 -8
- package/extensions/nes/nes-core.ts +29 -9
- package/extensions/nes/paths.ts +40 -0
- package/extensions/nes/renderer.ts +53 -88
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
120
|
+
if (!this.hasSram) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return this.nes.getSram();
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
setSram(
|
|
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
|
-
|
|
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
|
|
135
|
-
const
|
|
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
|
|
182
|
-
const
|
|
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
|
|
239
|
-
const
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|