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,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixel format conversion utilities for libretro cores
|
|
3
|
+
* Converts from libretro pixel formats to RGB24
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RETRO_PIXEL_FORMAT, FRAMEBUFFER_HEADROOM, RGB24_BYTES_PER_PIXEL } from "..";
|
|
7
|
+
|
|
8
|
+
import { logger } from "@/utils/logger";
|
|
9
|
+
|
|
10
|
+
// Pixel-format specific constants
|
|
11
|
+
import {
|
|
12
|
+
XRGB8888_BYTES_PER_PIXEL,
|
|
13
|
+
RGB16_BYTES_PER_PIXEL,
|
|
14
|
+
MASK_5BIT,
|
|
15
|
+
MASK_6BIT,
|
|
16
|
+
RGB565_RED_SHIFT,
|
|
17
|
+
RGB565_GREEN_SHIFT,
|
|
18
|
+
XRGB1555_RED_SHIFT,
|
|
19
|
+
XRGB1555_GREEN_SHIFT,
|
|
20
|
+
SCALE_5BIT_TO_8BIT_SHIFT,
|
|
21
|
+
SCALE_6BIT_TO_8BIT_SHIFT,
|
|
22
|
+
REPLICATE_5BIT_SHIFT,
|
|
23
|
+
REPLICATE_6BIT_SHIFT,
|
|
24
|
+
} from "./consts";
|
|
25
|
+
|
|
26
|
+
export * from './consts';
|
|
27
|
+
|
|
28
|
+
// Reusable output buffer to avoid allocations per frame
|
|
29
|
+
let outputBuffer: Uint8Array | null = null;
|
|
30
|
+
let outputBufferCapacity = 0;
|
|
31
|
+
|
|
32
|
+
/** Region bounds for cropped conversion */
|
|
33
|
+
interface ConvertBounds {
|
|
34
|
+
top: number;
|
|
35
|
+
left: number;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert a libretro framebuffer to RGB24 format
|
|
42
|
+
*
|
|
43
|
+
* @param data Raw framebuffer data from the core
|
|
44
|
+
* @param width Frame width in pixels (full source frame)
|
|
45
|
+
* @param height Frame height in pixels (full source frame)
|
|
46
|
+
* @param pitch Row stride in bytes (may be larger than width * bytesPerPixel)
|
|
47
|
+
* @param format Pixel format (XRGB1555, RGB565, or XRGB8888)
|
|
48
|
+
* @param bounds Optional region to convert (if omitted, converts full frame)
|
|
49
|
+
* @returns RGB24 framebuffer (3 bytes per pixel: R, G, B)
|
|
50
|
+
*/
|
|
51
|
+
export const convertFramebuffer = (
|
|
52
|
+
data: Uint8Array,
|
|
53
|
+
width: number,
|
|
54
|
+
height: number,
|
|
55
|
+
pitch: number,
|
|
56
|
+
format: number,
|
|
57
|
+
bounds?: ConvertBounds
|
|
58
|
+
): Uint8Array => {
|
|
59
|
+
// Use bounds if provided, otherwise convert full frame
|
|
60
|
+
const outWidth = bounds?.width ?? width;
|
|
61
|
+
const outHeight = bounds?.height ?? height;
|
|
62
|
+
const outputSize = outWidth * outHeight * RGB24_BYTES_PER_PIXEL;
|
|
63
|
+
|
|
64
|
+
// Reuse buffer if possible
|
|
65
|
+
if (!outputBuffer || outputBufferCapacity < outputSize) {
|
|
66
|
+
outputBufferCapacity = outputSize + FRAMEBUFFER_HEADROOM;
|
|
67
|
+
outputBuffer = new Uint8Array(outputBufferCapacity);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const output = outputBuffer;
|
|
71
|
+
|
|
72
|
+
switch (format) {
|
|
73
|
+
case RETRO_PIXEL_FORMAT.XRGB8888:
|
|
74
|
+
convertXRGB8888(data, width, pitch, output, bounds);
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case RETRO_PIXEL_FORMAT.RGB565:
|
|
78
|
+
convertRGB565(data, width, pitch, output, bounds);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case RETRO_PIXEL_FORMAT.XRGB1555:
|
|
82
|
+
default:
|
|
83
|
+
convertXRGB1555(data, width, pitch, output, bounds);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Return a view of just the used portion
|
|
88
|
+
return output.subarray(0, outputSize);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convert XRGB8888 (32-bit) to RGB24
|
|
93
|
+
* Format: XXXXXXXX RRRRRRRR GGGGGGGG BBBBBBBB (little-endian: B G R X)
|
|
94
|
+
*/
|
|
95
|
+
const convertXRGB8888 = (
|
|
96
|
+
data: Uint8Array,
|
|
97
|
+
sourceWidth: number,
|
|
98
|
+
pitch: number,
|
|
99
|
+
output: Uint8Array,
|
|
100
|
+
bounds?: ConvertBounds
|
|
101
|
+
): void => {
|
|
102
|
+
const startX = bounds?.left ?? 0;
|
|
103
|
+
const startY = bounds?.top ?? 0;
|
|
104
|
+
const outWidth = bounds?.width ?? sourceWidth;
|
|
105
|
+
const outHeight = bounds?.height ?? Math.floor(data.length / pitch);
|
|
106
|
+
let outIdx = 0;
|
|
107
|
+
|
|
108
|
+
for (let y = 0; y < outHeight; y++) {
|
|
109
|
+
const srcY = startY + y;
|
|
110
|
+
const rowOffset = srcY * pitch;
|
|
111
|
+
|
|
112
|
+
for (let x = 0; x < outWidth; x++) {
|
|
113
|
+
const srcX = startX + x;
|
|
114
|
+
const idx = rowOffset + srcX * XRGB8888_BYTES_PER_PIXEL;
|
|
115
|
+
// Little-endian: B at idx+0, G at idx+1, R at idx+2, X at idx+3
|
|
116
|
+
output[outIdx++] = data[idx + 2]; // R
|
|
117
|
+
output[outIdx++] = data[idx + 1]; // G
|
|
118
|
+
output[outIdx++] = data[idx]; // B
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Convert RGB565 (16-bit) to RGB24
|
|
125
|
+
* Format: RRRRRGGG GGGBBBBB (little-endian)
|
|
126
|
+
* Uses DataView for efficient 16-bit reads.
|
|
127
|
+
*/
|
|
128
|
+
const convertRGB565 = (
|
|
129
|
+
data: Uint8Array,
|
|
130
|
+
sourceWidth: number,
|
|
131
|
+
pitch: number,
|
|
132
|
+
output: Uint8Array,
|
|
133
|
+
bounds?: ConvertBounds
|
|
134
|
+
): void => {
|
|
135
|
+
const startX = bounds?.left ?? 0;
|
|
136
|
+
const startY = bounds?.top ?? 0;
|
|
137
|
+
const outWidth = bounds?.width ?? sourceWidth;
|
|
138
|
+
const outHeight = bounds?.height ?? Math.floor(data.length / pitch);
|
|
139
|
+
let outIdx = 0;
|
|
140
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
141
|
+
|
|
142
|
+
for (let y = 0; y < outHeight; y++) {
|
|
143
|
+
const srcY = startY + y;
|
|
144
|
+
const rowOffset = srcY * pitch;
|
|
145
|
+
|
|
146
|
+
for (let x = 0; x < outWidth; x++) {
|
|
147
|
+
const srcX = startX + x;
|
|
148
|
+
const idx = rowOffset + srcX * RGB16_BYTES_PER_PIXEL;
|
|
149
|
+
const pixel = view.getUint16(idx, true); // true = little-endian
|
|
150
|
+
|
|
151
|
+
// Extract 5-bit R, 6-bit G, 5-bit B
|
|
152
|
+
const r5 = (pixel >> RGB565_RED_SHIFT) & MASK_5BIT;
|
|
153
|
+
const g6 = (pixel >> RGB565_GREEN_SHIFT) & MASK_6BIT;
|
|
154
|
+
const b5 = pixel & MASK_5BIT;
|
|
155
|
+
|
|
156
|
+
// Scale to 8-bit (replicate upper bits into lower bits for accuracy)
|
|
157
|
+
output[outIdx++] = (r5 << SCALE_5BIT_TO_8BIT_SHIFT) | (r5 >> REPLICATE_5BIT_SHIFT);
|
|
158
|
+
output[outIdx++] = (g6 << SCALE_6BIT_TO_8BIT_SHIFT) | (g6 >> REPLICATE_6BIT_SHIFT);
|
|
159
|
+
output[outIdx++] = (b5 << SCALE_5BIT_TO_8BIT_SHIFT) | (b5 >> REPLICATE_5BIT_SHIFT);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert XRGB1555 (15-bit) to RGB24
|
|
166
|
+
* Format: XRRRRRGG GGGBBBBB (little-endian, X bit is ignored)
|
|
167
|
+
* Uses DataView for efficient 16-bit reads.
|
|
168
|
+
*/
|
|
169
|
+
const convertXRGB1555 = (
|
|
170
|
+
data: Uint8Array,
|
|
171
|
+
sourceWidth: number,
|
|
172
|
+
pitch: number,
|
|
173
|
+
output: Uint8Array,
|
|
174
|
+
bounds?: ConvertBounds
|
|
175
|
+
): void => {
|
|
176
|
+
const startX = bounds?.left ?? 0;
|
|
177
|
+
const startY = bounds?.top ?? 0;
|
|
178
|
+
const outWidth = bounds?.width ?? sourceWidth;
|
|
179
|
+
const outHeight = bounds?.height ?? Math.floor(data.length / pitch);
|
|
180
|
+
let outIdx = 0;
|
|
181
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
182
|
+
|
|
183
|
+
for (let y = 0; y < outHeight; y++) {
|
|
184
|
+
const srcY = startY + y;
|
|
185
|
+
const rowOffset = srcY * pitch;
|
|
186
|
+
|
|
187
|
+
for (let x = 0; x < outWidth; x++) {
|
|
188
|
+
const srcX = startX + x;
|
|
189
|
+
const idx = rowOffset + srcX * RGB16_BYTES_PER_PIXEL;
|
|
190
|
+
const pixel = view.getUint16(idx, true); // true = little-endian
|
|
191
|
+
|
|
192
|
+
// Extract 5-bit R, G, B (bit 15 is X, ignored)
|
|
193
|
+
const r5 = (pixel >> XRGB1555_RED_SHIFT) & MASK_5BIT;
|
|
194
|
+
const g5 = (pixel >> XRGB1555_GREEN_SHIFT) & MASK_5BIT;
|
|
195
|
+
const b5 = pixel & MASK_5BIT;
|
|
196
|
+
|
|
197
|
+
// Scale to 8-bit (replicate upper bits into lower bits for accuracy)
|
|
198
|
+
output[outIdx++] = (r5 << SCALE_5BIT_TO_8BIT_SHIFT) | (r5 >> REPLICATE_5BIT_SHIFT);
|
|
199
|
+
output[outIdx++] = (g5 << SCALE_5BIT_TO_8BIT_SHIFT) | (g5 >> REPLICATE_5BIT_SHIFT);
|
|
200
|
+
output[outIdx++] = (b5 << SCALE_5BIT_TO_8BIT_SHIFT) | (b5 >> REPLICATE_5BIT_SHIFT);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get bytes per pixel for a given format
|
|
207
|
+
*/
|
|
208
|
+
export const getBytesPerPixel = (format: number): number => {
|
|
209
|
+
switch (format) {
|
|
210
|
+
case RETRO_PIXEL_FORMAT.XRGB8888:
|
|
211
|
+
return XRGB8888_BYTES_PER_PIXEL;
|
|
212
|
+
case RETRO_PIXEL_FORMAT.RGB565:
|
|
213
|
+
case RETRO_PIXEL_FORMAT.XRGB1555:
|
|
214
|
+
default:
|
|
215
|
+
return RGB16_BYTES_PER_PIXEL;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get the format name for debugging
|
|
221
|
+
*/
|
|
222
|
+
export const getFormatName = (format: number): string => {
|
|
223
|
+
switch (format) {
|
|
224
|
+
case RETRO_PIXEL_FORMAT.XRGB8888:
|
|
225
|
+
return "XRGB8888";
|
|
226
|
+
case RETRO_PIXEL_FORMAT.RGB565:
|
|
227
|
+
return "RGB565";
|
|
228
|
+
case RETRO_PIXEL_FORMAT.XRGB1555:
|
|
229
|
+
return "XRGB1555";
|
|
230
|
+
default:
|
|
231
|
+
return `Unknown(${format})`;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
/** Content bounds detected in framebuffer */
|
|
236
|
+
export interface ContentBounds {
|
|
237
|
+
top: number;
|
|
238
|
+
bottom: number;
|
|
239
|
+
left: number;
|
|
240
|
+
right: number;
|
|
241
|
+
width: number;
|
|
242
|
+
height: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Threshold for considering a row/column as blank (0-255) */
|
|
246
|
+
const BLANK_VARIANCE_THRESHOLD = 10;
|
|
247
|
+
|
|
248
|
+
/** Minimum samples to check per row for blank detection */
|
|
249
|
+
const SAMPLES_PER_ROW = 16;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Detect actual content bounds in an RGB24 framebuffer.
|
|
253
|
+
* Finds the region of the frame that contains non-uniform content,
|
|
254
|
+
* excluding blank/solid color borders that some systems output.
|
|
255
|
+
*
|
|
256
|
+
* @param data RGB24 framebuffer (3 bytes per pixel: R, G, B)
|
|
257
|
+
* @param width Frame width in pixels
|
|
258
|
+
* @param height Frame height in pixels
|
|
259
|
+
* @returns Content bounds or null if entire frame appears uniform
|
|
260
|
+
*/
|
|
261
|
+
export const detectContentBounds = (data: Uint8Array, width: number, height: number): ContentBounds | null => {
|
|
262
|
+
// Find top bound (first non-blank row from top)
|
|
263
|
+
let top = 0;
|
|
264
|
+
for (let y = 0; y < height; y++) {
|
|
265
|
+
if (!isRowBlank(data, width, y)) {
|
|
266
|
+
top = y;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Find bottom bound (first non-blank row from bottom)
|
|
272
|
+
let bottom = height - 1;
|
|
273
|
+
for (let y = height - 1; y >= top; y--) {
|
|
274
|
+
if (!isRowBlank(data, width, y)) {
|
|
275
|
+
bottom = y;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Find left bound (first non-blank column from left)
|
|
281
|
+
let left = 0;
|
|
282
|
+
for (let x = 0; x < width; x++) {
|
|
283
|
+
if (!isColumnBlank(data, width, height, x, top, bottom)) {
|
|
284
|
+
left = x;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Find right bound (first non-blank column from right)
|
|
290
|
+
let right = width - 1;
|
|
291
|
+
for (let x = width - 1; x >= left; x--) {
|
|
292
|
+
if (!isColumnBlank(data, width, height, x, top, bottom)) {
|
|
293
|
+
right = x;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const contentWidth = right - left + 1;
|
|
299
|
+
const contentHeight = bottom - top + 1;
|
|
300
|
+
|
|
301
|
+
// Debug: log detected bounds
|
|
302
|
+
logger.debug(
|
|
303
|
+
`detectContentBounds: top=${top}, bottom=${bottom}, left=${left}, right=${right} (content: ${contentWidth}x${contentHeight} of ${width}x${height})`,
|
|
304
|
+
'Core'
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
// If detected bounds are the same as original, no cropping needed
|
|
308
|
+
if (contentWidth === width && contentHeight === height) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Only return bounds if we found meaningful content area
|
|
313
|
+
// (at least 25% of original dimensions to avoid false positives)
|
|
314
|
+
const MIN_CONTENT_RATIO = 0.25;
|
|
315
|
+
if (contentWidth < width * MIN_CONTENT_RATIO || contentHeight < height * MIN_CONTENT_RATIO) {
|
|
316
|
+
logger.debug('detectContentBounds: content too small, rejecting', 'Core');
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { top, bottom, left, right, width: contentWidth, height: contentHeight };
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if a row is "blank" (uniform color or very low variance)
|
|
325
|
+
*/
|
|
326
|
+
const isRowBlank = (data: Uint8Array, width: number, y: number): boolean => {
|
|
327
|
+
const rowStart = y * width * RGB24_BYTES_PER_PIXEL;
|
|
328
|
+
|
|
329
|
+
// Sample pixels across the row
|
|
330
|
+
const step = Math.max(1, Math.floor(width / SAMPLES_PER_ROW));
|
|
331
|
+
const firstR = data[rowStart];
|
|
332
|
+
const firstG = data[rowStart + 1];
|
|
333
|
+
const firstB = data[rowStart + 2];
|
|
334
|
+
|
|
335
|
+
for (let x = step; x < width; x += step) {
|
|
336
|
+
const idx = rowStart + x * RGB24_BYTES_PER_PIXEL;
|
|
337
|
+
const dr = Math.abs(data[idx] - firstR);
|
|
338
|
+
const dg = Math.abs(data[idx + 1] - firstG);
|
|
339
|
+
const db = Math.abs(data[idx + 2] - firstB);
|
|
340
|
+
|
|
341
|
+
if (dr > BLANK_VARIANCE_THRESHOLD || dg > BLANK_VARIANCE_THRESHOLD || db > BLANK_VARIANCE_THRESHOLD) {
|
|
342
|
+
return false; // Row has varied content
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return true; // Row is uniform
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if a column is "blank" (uniform color) within the content area
|
|
351
|
+
*/
|
|
352
|
+
const isColumnBlank = (data: Uint8Array, width: number, _height: number, x: number, top: number, bottom: number): boolean => {
|
|
353
|
+
const firstIdx = (top * width + x) * RGB24_BYTES_PER_PIXEL;
|
|
354
|
+
const firstR = data[firstIdx];
|
|
355
|
+
const firstG = data[firstIdx + 1];
|
|
356
|
+
const firstB = data[firstIdx + 2];
|
|
357
|
+
|
|
358
|
+
// Sample pixels down the column
|
|
359
|
+
const colHeight = bottom - top + 1;
|
|
360
|
+
const step = Math.max(1, Math.floor(colHeight / SAMPLES_PER_ROW));
|
|
361
|
+
|
|
362
|
+
for (let y = top + step; y <= bottom; y += step) {
|
|
363
|
+
const idx = (y * width + x) * RGB24_BYTES_PER_PIXEL;
|
|
364
|
+
const dr = Math.abs(data[idx] - firstR);
|
|
365
|
+
const dg = Math.abs(data[idx + 1] - firstG);
|
|
366
|
+
const db = Math.abs(data[idx + 2] - firstB);
|
|
367
|
+
|
|
368
|
+
if (dr > BLANK_VARIANCE_THRESHOLD || dg > BLANK_VARIANCE_THRESHOLD || db > BLANK_VARIANCE_THRESHOLD) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return true;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Check if an RGB24 framebuffer has any actual content (non-uniform pixels).
|
|
378
|
+
* Used to determine if a frame is worth analyzing for content bounds,
|
|
379
|
+
* or if it's still a blank/loading frame that should be skipped.
|
|
380
|
+
*
|
|
381
|
+
* @param data RGB24 framebuffer (3 bytes per pixel: R, G, B)
|
|
382
|
+
* @param width Frame width in pixels
|
|
383
|
+
* @param height Frame height in pixels
|
|
384
|
+
* @returns true if frame has varied content, false if entirely uniform
|
|
385
|
+
*/
|
|
386
|
+
export const hasFrameContent = (data: Uint8Array, width: number, height: number): boolean => {
|
|
387
|
+
// Sample rows across the frame to check for any non-uniform content
|
|
388
|
+
const rowStep = Math.max(1, Math.floor(height / SAMPLES_PER_ROW));
|
|
389
|
+
|
|
390
|
+
for (let y = 0; y < height; y += rowStep) {
|
|
391
|
+
if (!isRowBlank(data, width, y)) {
|
|
392
|
+
return true; // Found a row with varied content
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return false; // All sampled rows are uniform (blank frame)
|
|
397
|
+
};
|