emoemu 0.1.0
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/.claude/settings.local.json +77 -0
- package/.node-version +1 -0
- package/CLAUDE.md +435 -0
- package/README.md +404 -0
- package/TODO.md +655 -0
- package/dist/index.cjs +25108 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25085 -0
- package/docs/building-libretro-cores-arm-mac.md +237 -0
- package/docs/config-file-format.md +488 -0
- package/docs/cores-trd.md +425 -0
- package/docs/headless-hardware-rendering-trd.md +676 -0
- package/docs/libretro-cores-trd.md +997 -0
- package/docs/mupen64-software-rendering-trd.md +751 -0
- package/docs/n64-support-trd.md +306 -0
- package/docs/native-rendering-trd.md +540 -0
- package/docs/native-ui-rendering-trd.md +1195 -0
- package/docs/netplay-trd.md +665 -0
- package/docs/retroarch-netplay-docs.md +277 -0
- package/docs/save-state-format.md +666 -0
- package/eslint.config.js +111 -0
- package/icon/icon.png +0 -0
- package/icon/icon.pxd +0 -0
- package/package.json +63 -0
- package/pnpm-workspace.yaml +10 -0
- package/src/Emulator/consts.ts +14 -0
- package/src/Emulator/index.ts +2496 -0
- package/src/Emulator/saveState/index.ts +155 -0
- package/src/Emulator/screenshot/index.ts +160 -0
- package/src/Emulator/terminalDimensions/index.ts +79 -0
- package/src/Emulator/types.ts +83 -0
- package/src/cli/commands/consts.ts +10 -0
- package/src/cli/commands/index.ts +462 -0
- package/src/cli/parseArgs/consts.ts +17 -0
- package/src/cli/parseArgs/index.ts +457 -0
- package/src/cli/parseArgs/types.ts +61 -0
- package/src/cli/runEmulator/index.ts +406 -0
- package/src/cli/runEmulator/types.ts +7 -0
- package/src/consts.ts +19 -0
- package/src/core/button/consts.ts +35 -0
- package/src/core/button/index.ts +123 -0
- package/src/core/core.ts +300 -0
- package/src/core/index.ts +19 -0
- package/src/cores/libretro/api/index.ts +334 -0
- package/src/cores/libretro/api/types.ts +148 -0
- package/src/cores/libretro/callbacks/consts.ts +41 -0
- package/src/cores/libretro/callbacks/index.ts +456 -0
- package/src/cores/libretro/consts.ts +45 -0
- package/src/cores/libretro/coreOptions/consts.ts +36 -0
- package/src/cores/libretro/coreOptions/index.ts +222 -0
- package/src/cores/libretro/environment/consts.ts +118 -0
- package/src/cores/libretro/environment/index.ts +1095 -0
- package/src/cores/libretro/index.ts +937 -0
- package/src/cores/libretro/loader/index.ts +496 -0
- package/src/cores/libretro/pixelFormat/consts.ts +43 -0
- package/src/cores/libretro/pixelFormat/index.ts +397 -0
- package/src/cores/libretro/types.ts +339 -0
- package/src/frontend/AudioManager/index.ts +420 -0
- package/src/frontend/SettingsManager/index.ts +250 -0
- package/src/frontend/config/index.ts +608 -0
- package/src/frontend/config/tests.ts +354 -0
- package/src/frontend/config/types.ts +36 -0
- package/src/frontend/consts.ts +114 -0
- package/src/frontend/coreBuilder/index.ts +644 -0
- package/src/frontend/coreBuilder/types.ts +15 -0
- package/src/frontend/coreDownloader/index.ts +620 -0
- package/src/frontend/coreDownloader/types.ts +17 -0
- package/src/frontend/corePreferences/index.ts +69 -0
- package/src/frontend/coreRegistry/index.ts +276 -0
- package/src/frontend/directoryCache/index.ts +75 -0
- package/src/frontend/index.ts +79 -0
- package/src/frontend/notifications/consts.ts +14 -0
- package/src/frontend/notifications/index.ts +250 -0
- package/src/frontend/playlist/consts.ts +168 -0
- package/src/frontend/playlist/index.ts +899 -0
- package/src/frontend/playlist/labelFormatter/consts.ts +15 -0
- package/src/frontend/playlist/labelFormatter/index.ts +155 -0
- package/src/frontend/playlist/labelFormatter/tests.ts +153 -0
- package/src/frontend/playlist/reader/index.ts +559 -0
- package/src/frontend/playlist/sync/index.ts +511 -0
- package/src/frontend/playlist/systemLookup/index.ts +233 -0
- package/src/frontend/playlist/utils/index.ts +50 -0
- package/src/frontend/romScanner/consts.ts +348 -0
- package/src/frontend/romScanner/index.ts +1957 -0
- package/src/frontend/saveServices/consts.ts +2 -0
- package/src/frontend/saveServices/index.ts +313 -0
- package/src/frontend/serviceProvider/index.ts +108 -0
- package/src/frontend/serviceProvider/types.ts +13 -0
- package/src/index.ts +428 -0
- package/src/input/Controller/consts.ts +50 -0
- package/src/input/Controller/index.ts +81 -0
- package/src/input/GamepadManager/consts.ts +22 -0
- package/src/input/GamepadManager/index.ts +418 -0
- package/src/input/InputManager/consts.ts +86 -0
- package/src/input/InputManager/index.ts +593 -0
- package/src/input/InputMapper/consts.ts +33 -0
- package/src/input/InputMapper/index.ts +436 -0
- package/src/input/consts.ts +410 -0
- package/src/input/gamepadProfiles/index.ts +1100 -0
- package/src/input/index.ts +38 -0
- package/src/input/inputUtils/index.ts +31 -0
- package/src/netplay/FrameBuffer/consts.ts +2 -0
- package/src/netplay/FrameBuffer/index.ts +364 -0
- package/src/netplay/FrameBuffer/tests.ts +286 -0
- package/src/netplay/InputBuffer/consts.ts +7 -0
- package/src/netplay/InputBuffer/index.ts +347 -0
- package/src/netplay/InputBuffer/tests.ts +166 -0
- package/src/netplay/NetplayClient/index.ts +976 -0
- package/src/netplay/NetplayConnection/index.ts +536 -0
- package/src/netplay/NetplayDiscovery/consts.ts +41 -0
- package/src/netplay/NetplayDiscovery/index.ts +525 -0
- package/src/netplay/NetplayServer/index.ts +1407 -0
- package/src/netplay/SyncManager/index.ts +984 -0
- package/src/netplay/SyncManager/tests.ts +419 -0
- package/src/netplay/consts.ts +371 -0
- package/src/netplay/crc32/consts.ts +14 -0
- package/src/netplay/crc32/index.ts +97 -0
- package/src/netplay/crc32/tests.ts +40 -0
- package/src/netplay/index.ts +41 -0
- package/src/netplay/netplayLogger/consts.ts +30 -0
- package/src/netplay/netplayLogger/index.ts +345 -0
- package/src/netplay/protocol/consts.ts +86 -0
- package/src/netplay/protocol/index.ts +1280 -0
- package/src/netplay/protocol/tests.ts +606 -0
- package/src/netplay/protocol/types.ts +20 -0
- package/src/netplay/types.ts +395 -0
- package/src/rendering/KittyRenderer/index.ts +616 -0
- package/src/rendering/NativeRenderer/index.ts +279 -0
- package/src/rendering/NativeRenderer/tests.ts +133 -0
- package/src/rendering/TerminalRenderer/index.ts +770 -0
- package/src/rendering/consts.ts +401 -0
- package/src/rendering/fonts/CozetteVector.ttf +0 -0
- package/src/rendering/index.ts +26 -0
- package/src/rendering/nativeUi/NativeWindowManager/index.ts +158 -0
- package/src/rendering/nativeUi/NativeWindowManager/tests.ts +81 -0
- package/src/rendering/nativeUi/consts.ts +6 -0
- package/src/rendering/nativeUi/index.ts +20 -0
- package/src/rendering/postProcessing/consts.ts +38 -0
- package/src/rendering/postProcessing/index.ts +923 -0
- package/src/rendering/shared/ansi/consts.ts +12 -0
- package/src/rendering/shared/ansi/index.ts +104 -0
- package/src/rendering/shared/consts.ts +2 -0
- package/src/rendering/shared/fitToTerminal/index.ts +67 -0
- package/src/ui/AddRomsPrompt/consts.ts +13 -0
- package/src/ui/AddRomsPrompt/index.tsx +781 -0
- package/src/ui/App/consts.ts +2 -0
- package/src/ui/App/index.tsx +456 -0
- package/src/ui/AppCapabilities/index.tsx +67 -0
- package/src/ui/ConfigContext/index.tsx +56 -0
- package/src/ui/CoreManager/consts.ts +11 -0
- package/src/ui/CoreManager/index.tsx +779 -0
- package/src/ui/CoreSelector/consts.ts +2 -0
- package/src/ui/CoreSelector/index.tsx +251 -0
- package/src/ui/DialogContainer/index.tsx +42 -0
- package/src/ui/DialogOptionsList/index.tsx +61 -0
- package/src/ui/DuplicateCrcPrompt/consts.ts +5 -0
- package/src/ui/DuplicateCrcPrompt/index.tsx +146 -0
- package/src/ui/GamepadContext/consts.ts +15 -0
- package/src/ui/GamepadContext/index.tsx +295 -0
- package/src/ui/NativeDialog/index.tsx +120 -0
- package/src/ui/NetplayDisconnectedDialog/index.tsx +93 -0
- package/src/ui/NetplayPauseMenu/consts.ts +2 -0
- package/src/ui/NetplayPauseMenu/index.tsx +97 -0
- package/src/ui/RomBrowser/NetplayPanel/consts.ts +24 -0
- package/src/ui/RomBrowser/NetplayPanel/index.tsx +520 -0
- package/src/ui/RomBrowser/SettingsPanel/index.tsx +478 -0
- package/src/ui/RomBrowser/consts.ts +61 -0
- package/src/ui/RomBrowser/index.tsx +1164 -0
- package/src/ui/RomBrowser/settingsConfig/index.ts +320 -0
- package/src/ui/RomBrowser/types.ts +67 -0
- package/src/ui/SaveStateDialog/consts.ts +2 -0
- package/src/ui/SaveStateDialog/index.tsx +225 -0
- package/src/ui/WarningDialog/index.tsx +113 -0
- package/src/ui/consts.ts +27 -0
- package/src/ui/hooks/useClearTerminal/consts.ts +2 -0
- package/src/ui/hooks/useClearTerminal/index.ts +37 -0
- package/src/ui/hooks/useDialogNavigation/index.ts +99 -0
- package/src/ui/hooks/useGamepad/consts.ts +21 -0
- package/src/ui/hooks/useGamepad/index.ts +194 -0
- package/src/ui/index.ts +27 -0
- package/src/utils/buffer/consts.ts +17 -0
- package/src/utils/buffer/index.ts +129 -0
- package/src/utils/color/consts.ts +58 -0
- package/src/utils/color/index.ts +183 -0
- package/src/utils/compression/consts.ts +50 -0
- package/src/utils/compression/index.ts +101 -0
- package/src/utils/consts.ts +2 -0
- package/src/utils/crc32/consts.ts +22 -0
- package/src/utils/crc32/index.ts +83 -0
- package/src/utils/ensureDirectory/index.ts +10 -0
- package/src/utils/format/consts.ts +8 -0
- package/src/utils/format/index.ts +53 -0
- package/src/utils/getErrorMessage/index.ts +10 -0
- package/src/utils/index.ts +113 -0
- package/src/utils/ini/index.ts +200 -0
- package/src/utils/kitty/consts.ts +13 -0
- package/src/utils/kitty/index.ts +181 -0
- package/src/utils/logger/consts.ts +35 -0
- package/src/utils/logger/index.ts +217 -0
- package/src/utils/paths/consts.ts +18 -0
- package/src/utils/paths/index.ts +151 -0
- package/src/utils/png/consts.ts +34 -0
- package/src/utils/png/index.ts +131 -0
- package/src/utils/readJsonFile/index.ts +16 -0
- package/src/utils/rotateLogFile/index.ts +44 -0
- package/src/utils/safeClose/index.ts +10 -0
- package/src/utils/terminal/consts.ts +8 -0
- package/src/utils/terminal/index.ts +102 -0
- package/src/utils/thumbnailRenderer/consts.ts +2 -0
- package/src/utils/thumbnailRenderer/index.ts +147 -0
- package/src/utils/typedError/index.ts +26 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import { clamp } from 'remeda';
|
|
2
|
+
import { appendFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { deflateSync } from 'zlib';
|
|
4
|
+
import { PostProcessingPipeline, type EffectOptions } from '../postProcessing';
|
|
5
|
+
import { buildGammaLUT, rgb15ToRgb24, calculateLuminance8 } from '../../utils/color';
|
|
6
|
+
import { getTerminalDimensions } from '../../utils/terminal';
|
|
7
|
+
import { APC, ST, moveCursor, clearScreen, clearLine, hideCursor, showCursor } from '../shared/ansi';
|
|
8
|
+
import { fitToTerminal } from '../shared/fitToTerminal';
|
|
9
|
+
import { PNG_SIGNATURE, createPngChunk } from '../../utils/png';
|
|
10
|
+
import { isRgb15Buffer, type FrameBuffer } from '../../core/core';
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_NATIVE_WIDTH,
|
|
13
|
+
DEFAULT_NATIVE_HEIGHT,
|
|
14
|
+
STATUS_LINE_ROWS,
|
|
15
|
+
CELL_WIDTH_PX,
|
|
16
|
+
CELL_HEIGHT_PX,
|
|
17
|
+
INITIAL_FULL_RENDER_FRAMES,
|
|
18
|
+
KITTY_CHUNK_SIZE,
|
|
19
|
+
PNG_BIT_DEPTH,
|
|
20
|
+
PNG_COLOR_TYPE_INDEXED,
|
|
21
|
+
PNG_COLOR_TYPE_RGB,
|
|
22
|
+
DEFAULT_PNG_COMPRESSION,
|
|
23
|
+
DEFAULT_RENDER_SCALE,
|
|
24
|
+
MIN_RENDER_SCALE,
|
|
25
|
+
MAX_RENDER_SCALE,
|
|
26
|
+
MEMORY_LOG_INTERVAL,
|
|
27
|
+
DEFAULT_GAMMA,
|
|
28
|
+
RGB24_BYTES_PER_PIXEL,
|
|
29
|
+
PNG_IHDR_LENGTH,
|
|
30
|
+
PNG_IHDR_HEIGHT_OFFSET,
|
|
31
|
+
BYTES_PER_KB,
|
|
32
|
+
PACK_RED_SHIFT,
|
|
33
|
+
PACK_GREEN_SHIFT,
|
|
34
|
+
PALETTE_BUFFER_SIZE,
|
|
35
|
+
} from '..';
|
|
36
|
+
|
|
37
|
+
// Debug logging for Kitty graphics issues
|
|
38
|
+
const DEBUG_KITTY = process.env.DEBUG_KITTY === '1';
|
|
39
|
+
const DEBUG_LOG_PATH = '/tmp/kitty-debug.log';
|
|
40
|
+
|
|
41
|
+
const debugLog = (msg: string): void => {
|
|
42
|
+
if (!DEBUG_KITTY) {return;}
|
|
43
|
+
const timestamp = new Date().toISOString();
|
|
44
|
+
appendFileSync(DEBUG_LOG_PATH, `[${timestamp}] ${msg}\n`);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Kitty graphics protocol renderer
|
|
48
|
+
// https://sw.kovidgoyal.net/kitty/graphics-protocol/
|
|
49
|
+
|
|
50
|
+
export interface KittyRendererOptions extends EffectOptions {
|
|
51
|
+
scale?: number; // Scale factor for the image (undefined = auto-fit to terminal)
|
|
52
|
+
sourceWidth?: number; // Source framebuffer width (default: 256)
|
|
53
|
+
sourceHeight?: number; // Source framebuffer height (default: 240)
|
|
54
|
+
colorSpace?: 'rgb15' | 'rgb24'; // Color format (default: rgb24)
|
|
55
|
+
pixelAspectRatio?: number; // Pixel aspect ratio for correct display (default: 1.0)
|
|
56
|
+
enableDiffRendering?: boolean; // Enable diff-based rendering optimization (default: true)
|
|
57
|
+
colorEnabled?: boolean; // When false, render in grayscale (default: true)
|
|
58
|
+
pngCompressionLevel?: number; // PNG compression level 1-9 (default: 1, higher = smaller but slower)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Color space type for frame conversion
|
|
62
|
+
type ColorSpace = 'rgb15' | 'rgb24';
|
|
63
|
+
|
|
64
|
+
export class KittyRenderer {
|
|
65
|
+
private scale: number; // Scale factor (0.25, 0.5, 1, 2, 3, etc.) - supports both up and downscaling
|
|
66
|
+
private imageId: number = 1;
|
|
67
|
+
private frameNumber: number = 0;
|
|
68
|
+
private displayCols: number;
|
|
69
|
+
private displayRows: number;
|
|
70
|
+
private offsetCol: number = 1; // Horizontal offset for centering
|
|
71
|
+
private offsetRow: number = 1; // Vertical offset for centering
|
|
72
|
+
// Source framebuffer dimensions
|
|
73
|
+
private sourceWidth: number;
|
|
74
|
+
private sourceHeight: number;
|
|
75
|
+
// Pixel aspect ratio for correct display (e.g., 8/7 for NES)
|
|
76
|
+
private pixelAspectRatio: number;
|
|
77
|
+
// Scaled image dimensions in pixels (integer multiple of source resolution)
|
|
78
|
+
private scaledWidth: number;
|
|
79
|
+
private scaledHeight: number;
|
|
80
|
+
// Pre-allocated RGB buffer for native resolution (before scaling, for post-processing)
|
|
81
|
+
private nativeRgbBuffer!: Uint8Array;
|
|
82
|
+
// Pre-allocated RGB buffer for scaled output
|
|
83
|
+
private scaledRgbBuffer!: Uint8Array;
|
|
84
|
+
// Pre-allocated row buffer for horizontal scaling
|
|
85
|
+
private scaledRowBuffer!: Uint8Array;
|
|
86
|
+
// Previous frame buffer for row-level memoization (skip unchanged rows)
|
|
87
|
+
// For rgb15: Uint16Array (2 bytes per pixel)
|
|
88
|
+
// For rgb24: Uint8Array (3 bytes per pixel)
|
|
89
|
+
private prevFrameBuffer: Uint8Array | Uint16Array;
|
|
90
|
+
// Cached PNG data from last encode
|
|
91
|
+
private pngBuffer: Buffer = Buffer.alloc(0);
|
|
92
|
+
// Color space for framebuffer interpretation
|
|
93
|
+
private colorSpace: ColorSpace;
|
|
94
|
+
// Diff-based rendering optimization
|
|
95
|
+
private enableDiffRendering: boolean;
|
|
96
|
+
// Color mode (true = full color, false = grayscale)
|
|
97
|
+
private colorEnabled: boolean;
|
|
98
|
+
// PNG compression level (1-9, higher = smaller but slower)
|
|
99
|
+
private pngCompressionLevel: number;
|
|
100
|
+
// Gamma lookup table for frame conversion
|
|
101
|
+
private gammaLUT: Uint8Array;
|
|
102
|
+
// Post-processing effects pipeline
|
|
103
|
+
private postProcessing: PostProcessingPipeline;
|
|
104
|
+
|
|
105
|
+
constructor(options: KittyRendererOptions = {}) {
|
|
106
|
+
this.sourceWidth = options.sourceWidth ?? DEFAULT_NATIVE_WIDTH;
|
|
107
|
+
this.sourceHeight = options.sourceHeight ?? DEFAULT_NATIVE_HEIGHT;
|
|
108
|
+
this.colorSpace = options.colorSpace ?? 'rgb24';
|
|
109
|
+
this.pixelAspectRatio = options.pixelAspectRatio ?? 1.0;
|
|
110
|
+
this.enableDiffRendering = options.enableDiffRendering ?? true;
|
|
111
|
+
this.colorEnabled = options.colorEnabled ?? true;
|
|
112
|
+
this.pngCompressionLevel = options.pngCompressionLevel ?? DEFAULT_PNG_COMPRESSION;
|
|
113
|
+
|
|
114
|
+
// Build gamma LUT for frame conversion (separate from post-processing)
|
|
115
|
+
const gamma = options.gamma ?? DEFAULT_GAMMA;
|
|
116
|
+
this.gammaLUT = buildGammaLUT(gamma);
|
|
117
|
+
|
|
118
|
+
// Create post-processing pipeline with effect options
|
|
119
|
+
this.postProcessing = new PostProcessingPipeline({
|
|
120
|
+
gamma,
|
|
121
|
+
scanlines: options.scanlines,
|
|
122
|
+
saturation: options.saturation,
|
|
123
|
+
brightness: options.brightness,
|
|
124
|
+
contrast: options.contrast,
|
|
125
|
+
vignette: options.vignette,
|
|
126
|
+
bloom: options.bloom,
|
|
127
|
+
bloomThreshold: options.bloomThreshold,
|
|
128
|
+
ntsc: options.ntsc,
|
|
129
|
+
curvature: options.curvature,
|
|
130
|
+
chromaticAberration: options.chromaticAberration,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Allocate prevFrameBuffer based on color space
|
|
134
|
+
const pixelCount = this.sourceWidth * this.sourceHeight;
|
|
135
|
+
if (this.colorSpace === 'rgb15') {
|
|
136
|
+
this.prevFrameBuffer = new Uint16Array(pixelCount);
|
|
137
|
+
} else {
|
|
138
|
+
this.prevFrameBuffer = new Uint8Array(pixelCount * RGB24_BYTES_PER_PIXEL);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Render scale controls internal buffer resolution (default: DEFAULT_RENDER_SCALE)
|
|
142
|
+
// Display size always fills terminal, Kitty handles the final scaling
|
|
143
|
+
// Supports fractional scales (0.25, 0.5) for downscaling and integer scales (1, 2, 3, 4) for upscaling
|
|
144
|
+
const rawScale = options.scale ?? DEFAULT_RENDER_SCALE;
|
|
145
|
+
this.scale = clamp(rawScale, { min: MIN_RENDER_SCALE, max: MAX_RENDER_SCALE });
|
|
146
|
+
|
|
147
|
+
// Calculate display size to fill terminal (Kitty will scale the image)
|
|
148
|
+
const { cols, rows, offsetCol, offsetRow } = this.calculateDisplaySize();
|
|
149
|
+
this.displayCols = cols;
|
|
150
|
+
this.displayRows = rows;
|
|
151
|
+
this.offsetCol = offsetCol;
|
|
152
|
+
this.offsetRow = offsetRow;
|
|
153
|
+
|
|
154
|
+
// Internal buffer uses render scale (round to ensure integer dimensions)
|
|
155
|
+
this.scaledWidth = Math.max(1, Math.round(this.sourceWidth * this.scale));
|
|
156
|
+
this.scaledHeight = Math.max(1, Math.round(this.sourceHeight * this.scale));
|
|
157
|
+
|
|
158
|
+
// Allocate buffers: native resolution for post-processing, scaled for output
|
|
159
|
+
this.nativeRgbBuffer = new Uint8Array(this.sourceWidth * this.sourceHeight * RGB24_BYTES_PER_PIXEL);
|
|
160
|
+
this.scaledRgbBuffer = new Uint8Array(this.scaledWidth * this.scaledHeight * RGB24_BYTES_PER_PIXEL);
|
|
161
|
+
this.scaledRowBuffer = new Uint8Array(this.scaledWidth * RGB24_BYTES_PER_PIXEL);
|
|
162
|
+
|
|
163
|
+
// Initialize debug log
|
|
164
|
+
if (DEBUG_KITTY) {
|
|
165
|
+
writeFileSync(DEBUG_LOG_PATH, `=== Kitty Debug Log Started ===\n`);
|
|
166
|
+
debugLog(`Init: sourceSize=${this.sourceWidth}x${this.sourceHeight}, scale=${this.scale}, scaledSize=${this.scaledWidth}x${this.scaledHeight}`);
|
|
167
|
+
debugLog(`Init: displayCols=${this.displayCols}, displayRows=${this.displayRows}, colorSpace=${this.colorSpace}`);
|
|
168
|
+
debugLog(`Init: pngCompression=${this.pngCompressionLevel}, diffRendering=${this.enableDiffRendering}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Cleanup method (no-op, kept for API compatibility)
|
|
173
|
+
destroy(): void {
|
|
174
|
+
debugLog(`Destroy: frameNumber=${this.frameNumber}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Calculate display size to fill terminal while maintaining aspect ratio
|
|
178
|
+
// Kitty handles scaling from internal buffer to display size
|
|
179
|
+
private calculateDisplaySize(): { cols: number; rows: number; offsetCol: number; offsetRow: number } {
|
|
180
|
+
const { width: termCols, height: termRows } = getTerminalDimensions();
|
|
181
|
+
|
|
182
|
+
const availableRows = termRows - STATUS_LINE_ROWS;
|
|
183
|
+
const availableCols = termCols;
|
|
184
|
+
|
|
185
|
+
// Formula: cols = rows * (sourceWidth * PAR) / sourceHeight * (cellHeight / cellWidth)
|
|
186
|
+
// For NES (256x240, 8:7 PAR): cols ≈ rows * 2.438
|
|
187
|
+
// For GBC (160x144, 1:1 PAR): cols ≈ rows * 2.222
|
|
188
|
+
const aspectRatio = (this.sourceWidth * this.pixelAspectRatio * CELL_HEIGHT_PX) / (this.sourceHeight * CELL_WIDTH_PX);
|
|
189
|
+
|
|
190
|
+
const { width: displayCols, height: displayRows } = fitToTerminal({
|
|
191
|
+
availableCols,
|
|
192
|
+
availableRows,
|
|
193
|
+
aspectRatio,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Calculate centering offsets (1-based for ANSI escape sequences)
|
|
197
|
+
const offsetCol = Math.max(1, Math.floor((termCols - displayCols) / 2) + 1);
|
|
198
|
+
const offsetRow = Math.max(1, Math.floor((availableRows - displayRows) / 2) + 1);
|
|
199
|
+
|
|
200
|
+
return { cols: displayCols, rows: displayRows, offsetCol, offsetRow };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Get current scale (useful for display info)
|
|
204
|
+
getScale(): number {
|
|
205
|
+
return this.scale;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get display dimensions
|
|
209
|
+
getDisplaySize(): { cols: number; rows: number } {
|
|
210
|
+
return { cols: this.displayCols, rows: this.displayRows };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Update display dimensions (for terminal resize handling)
|
|
214
|
+
// Internal buffer scale stays fixed, only display size changes
|
|
215
|
+
setDimensions(): void {
|
|
216
|
+
const { cols, rows, offsetCol, offsetRow } = this.calculateDisplaySize();
|
|
217
|
+
this.displayCols = cols;
|
|
218
|
+
this.displayRows = rows;
|
|
219
|
+
this.offsetCol = offsetCol;
|
|
220
|
+
this.offsetRow = offsetRow;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Generic frame comparison - check if entire frame is unchanged
|
|
224
|
+
private isFrameUnchanged(frameBuffer: Uint8Array | Uint16Array): boolean {
|
|
225
|
+
const prev = this.prevFrameBuffer;
|
|
226
|
+
const len = frameBuffer.length;
|
|
227
|
+
for (let i = 0; i < len; i++) {
|
|
228
|
+
if (frameBuffer[i] !== prev[i]) {return false;}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Convert frame to RGB at native resolution (no scaling)
|
|
234
|
+
// Handles all color spaces: rgb15, rgb24
|
|
235
|
+
private frameToRgbNative(
|
|
236
|
+
frameBuffer: FrameBuffer,
|
|
237
|
+
colorSpace: ColorSpace
|
|
238
|
+
): void {
|
|
239
|
+
const dst = this.nativeRgbBuffer;
|
|
240
|
+
const gammaLUT = this.gammaLUT;
|
|
241
|
+
const colorEnabled = this.colorEnabled;
|
|
242
|
+
const width = this.sourceWidth;
|
|
243
|
+
const height = this.sourceHeight;
|
|
244
|
+
|
|
245
|
+
for (let y = 0; y < height; y++) {
|
|
246
|
+
const srcRowStart = colorSpace === 'rgb24'
|
|
247
|
+
? y * width * RGB24_BYTES_PER_PIXEL
|
|
248
|
+
: y * width;
|
|
249
|
+
const dstRowStart = y * width * RGB24_BYTES_PER_PIXEL;
|
|
250
|
+
|
|
251
|
+
for (let x = 0; x < width; x++) {
|
|
252
|
+
let r: number, g: number, b: number;
|
|
253
|
+
|
|
254
|
+
if (isRgb15Buffer(colorSpace, frameBuffer)) {
|
|
255
|
+
// RGB15: XBBBBBGGGGGRRRRR (5 bits per channel)
|
|
256
|
+
const color = frameBuffer[srcRowStart + x];
|
|
257
|
+
const [r8, g8, b8] = rgb15ToRgb24(color);
|
|
258
|
+
r = gammaLUT[r8];
|
|
259
|
+
g = gammaLUT[g8];
|
|
260
|
+
b = gammaLUT[b8];
|
|
261
|
+
} else {
|
|
262
|
+
// RGB24: direct 8-bit channels
|
|
263
|
+
const srcIdx = srcRowStart + x * RGB24_BYTES_PER_PIXEL;
|
|
264
|
+
r = gammaLUT[frameBuffer[srcIdx]];
|
|
265
|
+
g = gammaLUT[frameBuffer[srcIdx + 1]];
|
|
266
|
+
b = gammaLUT[frameBuffer[srcIdx + 2]];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Convert to grayscale if color is disabled
|
|
270
|
+
const dstIdx = dstRowStart + x * RGB24_BYTES_PER_PIXEL;
|
|
271
|
+
if (!colorEnabled) {
|
|
272
|
+
const gray = calculateLuminance8(r, g, b);
|
|
273
|
+
dst[dstIdx] = gray;
|
|
274
|
+
dst[dstIdx + 1] = gray;
|
|
275
|
+
dst[dstIdx + 2] = gray;
|
|
276
|
+
} else {
|
|
277
|
+
dst[dstIdx] = r;
|
|
278
|
+
dst[dstIdx + 1] = g;
|
|
279
|
+
dst[dstIdx + 2] = b;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Scale RGB buffer from native to display resolution
|
|
286
|
+
// Supports both upscaling (scale > 1) and downscaling (scale < 1)
|
|
287
|
+
private scaleRgbBuffer(): void {
|
|
288
|
+
const src = this.nativeRgbBuffer;
|
|
289
|
+
const dst = this.scaledRgbBuffer;
|
|
290
|
+
const scale = this.scale;
|
|
291
|
+
const srcWidth = this.sourceWidth;
|
|
292
|
+
const srcHeight = this.sourceHeight;
|
|
293
|
+
const dstWidth = this.scaledWidth;
|
|
294
|
+
const dstHeight = this.scaledHeight;
|
|
295
|
+
|
|
296
|
+
if (scale >= 1) {
|
|
297
|
+
// Upscaling: duplicate pixels (integer scale path)
|
|
298
|
+
const intScale = Math.round(scale);
|
|
299
|
+
const dstRowBytes = dstWidth * RGB24_BYTES_PER_PIXEL;
|
|
300
|
+
const rowBuffer = this.scaledRowBuffer;
|
|
301
|
+
|
|
302
|
+
for (let srcY = 0; srcY < srcHeight; srcY++) {
|
|
303
|
+
const srcRowStart = srcY * srcWidth * RGB24_BYTES_PER_PIXEL;
|
|
304
|
+
|
|
305
|
+
// Scale one source row horizontally into rowBuffer
|
|
306
|
+
let rowIdx = 0;
|
|
307
|
+
for (let srcX = 0; srcX < srcWidth; srcX++) {
|
|
308
|
+
const srcIdx = srcRowStart + srcX * RGB24_BYTES_PER_PIXEL;
|
|
309
|
+
const r = src[srcIdx];
|
|
310
|
+
const g = src[srcIdx + 1];
|
|
311
|
+
const b = src[srcIdx + 2];
|
|
312
|
+
|
|
313
|
+
// Write pixel 'scale' times horizontally
|
|
314
|
+
for (let sx = 0; sx < intScale; sx++) {
|
|
315
|
+
rowBuffer[rowIdx] = r;
|
|
316
|
+
rowBuffer[rowIdx + 1] = g;
|
|
317
|
+
rowBuffer[rowIdx + 2] = b;
|
|
318
|
+
rowIdx += RGB24_BYTES_PER_PIXEL;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Copy the scaled row 'scale' times vertically
|
|
323
|
+
const dstRowStart = srcY * intScale * dstRowBytes;
|
|
324
|
+
for (let sy = 0; sy < intScale; sy++) {
|
|
325
|
+
dst.set(rowBuffer, dstRowStart + sy * dstRowBytes);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// Downscaling: sample pixels using nearest-neighbor
|
|
330
|
+
// For each destination pixel, find the corresponding source pixel
|
|
331
|
+
const invScale = 1 / scale;
|
|
332
|
+
|
|
333
|
+
for (let dstY = 0; dstY < dstHeight; dstY++) {
|
|
334
|
+
const srcY = Math.min(Math.floor(dstY * invScale), srcHeight - 1);
|
|
335
|
+
const srcRowStart = srcY * srcWidth * RGB24_BYTES_PER_PIXEL;
|
|
336
|
+
const dstRowStart = dstY * dstWidth * RGB24_BYTES_PER_PIXEL;
|
|
337
|
+
|
|
338
|
+
for (let dstX = 0; dstX < dstWidth; dstX++) {
|
|
339
|
+
const srcX = Math.min(Math.floor(dstX * invScale), srcWidth - 1);
|
|
340
|
+
const srcIdx = srcRowStart + srcX * RGB24_BYTES_PER_PIXEL;
|
|
341
|
+
const dstIdx = dstRowStart + dstX * RGB24_BYTES_PER_PIXEL;
|
|
342
|
+
|
|
343
|
+
dst[dstIdx] = src[srcIdx];
|
|
344
|
+
dst[dstIdx + 1] = src[srcIdx + 1];
|
|
345
|
+
dst[dstIdx + 2] = src[srcIdx + 2];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Encode scaled RGB buffer to PNG format (indexed color with fallback to RGB)
|
|
352
|
+
private encodePng(): void {
|
|
353
|
+
const width = this.scaledWidth;
|
|
354
|
+
const height = this.scaledHeight;
|
|
355
|
+
const rgbData = this.scaledRgbBuffer;
|
|
356
|
+
const pixelCount = width * height;
|
|
357
|
+
|
|
358
|
+
// Build palette by scanning all pixels
|
|
359
|
+
// Use a Map with packed RGB as key for fast lookup
|
|
360
|
+
const colorToIndex = new Map<number, number>();
|
|
361
|
+
const palette: number[] = []; // Flat RGB array
|
|
362
|
+
let fallbackToRgb = false;
|
|
363
|
+
|
|
364
|
+
// First pass: collect unique colors
|
|
365
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
366
|
+
const idx = i * RGB24_BYTES_PER_PIXEL;
|
|
367
|
+
const r = rgbData[idx];
|
|
368
|
+
const g = rgbData[idx + 1];
|
|
369
|
+
const b = rgbData[idx + 2];
|
|
370
|
+
const packed = (r << PACK_RED_SHIFT) | (g << PACK_GREEN_SHIFT) | b;
|
|
371
|
+
|
|
372
|
+
if (!colorToIndex.has(packed)) {
|
|
373
|
+
if (palette.length >= PALETTE_BUFFER_SIZE) {
|
|
374
|
+
fallbackToRgb = true;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
colorToIndex.set(packed, palette.length / RGB24_BYTES_PER_PIXEL);
|
|
378
|
+
palette.push(r, g, b);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (fallbackToRgb) {
|
|
383
|
+
// More than 256 colors - fall back to RGB encoding
|
|
384
|
+
this.encodePngRgb();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Second pass: build indexed pixel data
|
|
389
|
+
const rawDataSize = height * (1 + width); // 1 filter byte + 1 byte per pixel per row
|
|
390
|
+
const rawData = Buffer.alloc(rawDataSize);
|
|
391
|
+
|
|
392
|
+
for (let y = 0; y < height; y++) {
|
|
393
|
+
const rawRowStart = y * (1 + width);
|
|
394
|
+
rawData[rawRowStart] = 0; // Filter type: none
|
|
395
|
+
|
|
396
|
+
for (let x = 0; x < width; x++) {
|
|
397
|
+
const srcIdx = (y * width + x) * RGB24_BYTES_PER_PIXEL;
|
|
398
|
+
const r = rgbData[srcIdx];
|
|
399
|
+
const g = rgbData[srcIdx + 1];
|
|
400
|
+
const b = rgbData[srcIdx + 2];
|
|
401
|
+
const packed = (r << PACK_RED_SHIFT) | (g << PACK_GREEN_SHIFT) | b;
|
|
402
|
+
rawData[rawRowStart + 1 + x] = colorToIndex.get(packed)!;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const compressed = deflateSync(rawData, { level: this.pngCompressionLevel });
|
|
407
|
+
|
|
408
|
+
// Build IHDR
|
|
409
|
+
const ihdr = Buffer.alloc(PNG_IHDR_LENGTH);
|
|
410
|
+
ihdr.writeUInt32BE(width, 0);
|
|
411
|
+
ihdr.writeUInt32BE(height, PNG_IHDR_HEIGHT_OFFSET);
|
|
412
|
+
ihdr[8] = PNG_BIT_DEPTH; // bit depth
|
|
413
|
+
ihdr[9] = PNG_COLOR_TYPE_INDEXED; // color type: indexed
|
|
414
|
+
ihdr[10] = 0; // compression
|
|
415
|
+
ihdr[11] = 0; // filter
|
|
416
|
+
ihdr[12] = 0; // interlace
|
|
417
|
+
|
|
418
|
+
// Build PLTE chunk
|
|
419
|
+
const plteData = Buffer.from(palette);
|
|
420
|
+
|
|
421
|
+
const ihdrChunk = createPngChunk('IHDR', ihdr);
|
|
422
|
+
const plteChunk = createPngChunk('PLTE', plteData);
|
|
423
|
+
const idatChunk = createPngChunk('IDAT', compressed);
|
|
424
|
+
const iendChunk = createPngChunk('IEND', Buffer.alloc(0));
|
|
425
|
+
|
|
426
|
+
this.pngBuffer = Buffer.concat([PNG_SIGNATURE, ihdrChunk, plteChunk, idatChunk, iendChunk]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Fallback RGB encoding when palette exceeds 256 colors
|
|
430
|
+
private encodePngRgb(): void {
|
|
431
|
+
const width = this.scaledWidth;
|
|
432
|
+
const height = this.scaledHeight;
|
|
433
|
+
const rgbData = this.scaledRgbBuffer;
|
|
434
|
+
|
|
435
|
+
const rowBytes = width * RGB24_BYTES_PER_PIXEL;
|
|
436
|
+
const rawDataSize = height * (1 + rowBytes);
|
|
437
|
+
const rawData = Buffer.alloc(rawDataSize);
|
|
438
|
+
|
|
439
|
+
for (let y = 0; y < height; y++) {
|
|
440
|
+
const rawRowStart = y * (1 + rowBytes);
|
|
441
|
+
rawData[rawRowStart] = 0; // Filter type: none
|
|
442
|
+
const srcStart = y * rowBytes;
|
|
443
|
+
for (let i = 0; i < rowBytes; i++) {
|
|
444
|
+
rawData[rawRowStart + 1 + i] = rgbData[srcStart + i];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const compressed = deflateSync(rawData, { level: this.pngCompressionLevel });
|
|
449
|
+
|
|
450
|
+
const ihdr = Buffer.alloc(PNG_IHDR_LENGTH);
|
|
451
|
+
ihdr.writeUInt32BE(width, 0);
|
|
452
|
+
ihdr.writeUInt32BE(height, PNG_IHDR_HEIGHT_OFFSET);
|
|
453
|
+
ihdr[8] = PNG_BIT_DEPTH; // bit depth
|
|
454
|
+
ihdr[9] = PNG_COLOR_TYPE_RGB; // color type: RGB
|
|
455
|
+
ihdr[10] = 0; // compression
|
|
456
|
+
ihdr[11] = 0; // filter
|
|
457
|
+
ihdr[12] = 0; // interlace
|
|
458
|
+
|
|
459
|
+
const ihdrChunk = createPngChunk('IHDR', ihdr);
|
|
460
|
+
const idatChunk = createPngChunk('IDAT', compressed);
|
|
461
|
+
const iendChunk = createPngChunk('IEND', Buffer.alloc(0));
|
|
462
|
+
|
|
463
|
+
this.pngBuffer = Buffer.concat([PNG_SIGNATURE, ihdrChunk, idatChunk, iendChunk]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Send image using Kitty graphics protocol with chunked transmission
|
|
467
|
+
private sendImage(): string {
|
|
468
|
+
// Encode to PNG (compressed)
|
|
469
|
+
this.encodePng();
|
|
470
|
+
const base64 = this.pngBuffer.toString('base64');
|
|
471
|
+
const chunks: string[] = [];
|
|
472
|
+
|
|
473
|
+
// Use large chunks to avoid crashes in some terminals (e.g., iTerm2)
|
|
474
|
+
// 4096 is Kitty's recommendation but causes issues with chunked transmission in some terminals
|
|
475
|
+
// Post-processing can create ~120KB+ base64, so use 256KB chunks
|
|
476
|
+
const chunkSize = KITTY_CHUNK_SIZE;
|
|
477
|
+
|
|
478
|
+
// Use alternating image IDs for double-buffering effect
|
|
479
|
+
const currentId = this.imageId + (this.frameNumber % 2);
|
|
480
|
+
const previousId = this.imageId + ((this.frameNumber + 1) % 2);
|
|
481
|
+
|
|
482
|
+
// Debug logging
|
|
483
|
+
const numChunks = Math.ceil(base64.length / chunkSize);
|
|
484
|
+
debugLog(`Frame ${this.frameNumber}: imageId=${currentId}, prevId=${previousId}, pngBytes=${this.pngBuffer.length}, base64Len=${base64.length}, chunks=${numChunks}`);
|
|
485
|
+
|
|
486
|
+
for (let i = 0; i < base64.length; i += chunkSize) {
|
|
487
|
+
const chunk = base64.slice(i, i + chunkSize);
|
|
488
|
+
const isFirst = i === 0;
|
|
489
|
+
const isLast = i + chunkSize >= base64.length;
|
|
490
|
+
|
|
491
|
+
let control: string;
|
|
492
|
+
|
|
493
|
+
if (isFirst) {
|
|
494
|
+
// First chunk: transmit image data
|
|
495
|
+
// a=T: transmit and display
|
|
496
|
+
// f=100: PNG format
|
|
497
|
+
// i=id: image ID
|
|
498
|
+
// p=1: placement ID (allows replacing in place)
|
|
499
|
+
// q=2: suppress response
|
|
500
|
+
// C=1: do not move cursor after displaying
|
|
501
|
+
// c=cols, r=rows: display size in terminal cells
|
|
502
|
+
// m=1: more chunks follow (0 if last)
|
|
503
|
+
const displayParams = `,c=${this.displayCols},r=${this.displayRows}`;
|
|
504
|
+
control = `a=T,f=100,i=${currentId},p=1,q=2,C=1${displayParams},m=${isLast ? 0 : 1}`;
|
|
505
|
+
debugLog(`Frame ${this.frameNumber}: SEND control="${control}"`);
|
|
506
|
+
} else {
|
|
507
|
+
// Subsequent chunks: just continuation
|
|
508
|
+
control = `m=${isLast ? 0 : 1}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
chunks.push(`${APC}${control};${chunk}${ST}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Delete previous frame's image after displaying new one
|
|
515
|
+
if (this.frameNumber > 0) {
|
|
516
|
+
debugLog(`Frame ${this.frameNumber}: DELETE imageId=${previousId}`);
|
|
517
|
+
chunks.push(`${APC}a=d,d=I,i=${previousId},q=2${ST}`);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
this.frameNumber++;
|
|
521
|
+
|
|
522
|
+
return chunks.join('');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Unified render method for all color spaces
|
|
526
|
+
private renderInternal(
|
|
527
|
+
frameBuffer: FrameBuffer,
|
|
528
|
+
colorSpace: ColorSpace
|
|
529
|
+
): string {
|
|
530
|
+
// Check if frame buffer size changed (can happen with SNES resolution changes)
|
|
531
|
+
// If so, reallocate prevFrameBuffer to match
|
|
532
|
+
if (frameBuffer.length !== this.prevFrameBuffer.length) {
|
|
533
|
+
if (frameBuffer instanceof Uint16Array) {
|
|
534
|
+
this.prevFrameBuffer = new Uint16Array(frameBuffer.length);
|
|
535
|
+
} else {
|
|
536
|
+
this.prevFrameBuffer = new Uint8Array(frameBuffer.length);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Force full rendering for initial frames to ensure display is populated
|
|
541
|
+
const isInitialFrame = this.frameNumber < INITIAL_FULL_RENDER_FRAMES;
|
|
542
|
+
|
|
543
|
+
// Periodic memory logging (every 60 frames ≈ 1 second)
|
|
544
|
+
if (DEBUG_KITTY && this.frameNumber % MEMORY_LOG_INTERVAL === 0) {
|
|
545
|
+
const mem = process.memoryUsage();
|
|
546
|
+
debugLog(`Memory @ frame ${this.frameNumber}: heapUsed=${Math.round(mem.heapUsed / BYTES_PER_KB / BYTES_PER_KB)}MB, heapTotal=${Math.round(mem.heapTotal / BYTES_PER_KB / BYTES_PER_KB)}MB, rss=${Math.round(mem.rss / BYTES_PER_KB / BYTES_PER_KB)}MB`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Skip entirely if frame unchanged (after initial frames)
|
|
550
|
+
if (this.enableDiffRendering && !isInitialFrame && this.isFrameUnchanged(frameBuffer)) {
|
|
551
|
+
debugLog(`Frame ${this.frameNumber}: SKIPPED (unchanged)`);
|
|
552
|
+
return '';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Convert frame to RGB at native resolution
|
|
556
|
+
this.frameToRgbNative(frameBuffer, colorSpace);
|
|
557
|
+
|
|
558
|
+
// Save current frame for next frame's diff check
|
|
559
|
+
this.prevFrameBuffer.set(frameBuffer);
|
|
560
|
+
|
|
561
|
+
// Apply post-processing effects at native resolution (much faster than scaled)
|
|
562
|
+
this.postProcessing.apply(this.nativeRgbBuffer, this.sourceWidth, this.sourceHeight);
|
|
563
|
+
|
|
564
|
+
// Scale up to display resolution
|
|
565
|
+
this.scaleRgbBuffer();
|
|
566
|
+
|
|
567
|
+
// Build output
|
|
568
|
+
let output = '';
|
|
569
|
+
|
|
570
|
+
// Move cursor to centered position for image placement
|
|
571
|
+
output += moveCursor(this.offsetRow, this.offsetCol);
|
|
572
|
+
|
|
573
|
+
// Always use PNG - compression helps with terminal I/O even for dynamic frames
|
|
574
|
+
output += this.sendImage();
|
|
575
|
+
|
|
576
|
+
return output;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Render RGB15 frame buffer to Kitty graphics
|
|
580
|
+
renderRgb15(frameBuffer: Uint16Array): string {
|
|
581
|
+
return this.renderInternal(frameBuffer, 'rgb15');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Render RGB24 frame buffer to Kitty graphics
|
|
585
|
+
renderRgb24(frameBuffer: Uint8Array): string {
|
|
586
|
+
return this.renderInternal(frameBuffer, 'rgb24');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Clear screen
|
|
590
|
+
clearScreen(): string {
|
|
591
|
+
// Delete all images and clear screen
|
|
592
|
+
return `${APC}a=d,d=A,q=2${ST}${clearScreen()}`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Hide cursor
|
|
596
|
+
hideCursor(): string {
|
|
597
|
+
return hideCursor();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Show cursor
|
|
601
|
+
showCursor(): string {
|
|
602
|
+
return showCursor();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Get status row (below the image)
|
|
606
|
+
getStatusRow(): number {
|
|
607
|
+
// We know exactly how many rows the image uses from displayRows
|
|
608
|
+
// Account for vertical centering offset
|
|
609
|
+
return this.offsetRow + this.displayRows;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Move cursor to status row
|
|
613
|
+
moveCursorToRow(row: number): string {
|
|
614
|
+
return moveCursor(row, 1) + clearLine();
|
|
615
|
+
}
|
|
616
|
+
}
|