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,1195 @@
|
|
|
1
|
+
# Native UI Rendering - Technical Requirements Document
|
|
2
|
+
|
|
3
|
+
This document describes the requirements and implementation approach for rendering the Ink-based UI (ROM browser, settings, dialogs) through a native window instead of the terminal.
|
|
4
|
+
|
|
5
|
+
> **Implementation note:** This design shipped via the [`ink-native`](https://www.npmjs.com/package/ink-native) package — a bundled `fenster` native window backend plus an embedded Cozette bitmap font, with **zero system dependencies** to install. The CLI flag is `--native` and the config value is `video_driver = "native"`. UI and game share a single native window created once via `createStreams()`; the app hands the window off between UI and game with `window.pause()` / `window.resume()`, and the game renderer writes each post-processed frame directly into ink-native's shared `Uint32Array` framebuffer (`0xAARRGGBB`, via `packColor` + `renderer.present()`). This replaced the custom-SDL2-bindings approach originally proposed in this document.
|
|
6
|
+
>
|
|
7
|
+
> **Known limitations of the shipped implementation:** no runtime `setTitle` (ink-native exposes none), no runtime scale-factor change (`menu_scale_factor` maps to ink-native's `scaleFactor`, which only applies at window creation), and no programmatic window resize (the game letterboxes into the fixed window instead).
|
|
8
|
+
>
|
|
9
|
+
> Aside from the "Overview", "Configuration", and "Dependencies" sections (updated to reflect what shipped), the remaining sections below — Implementation Approaches, Text Rendering internals, SDL Bindings Extensions, ANSI Sequence Parsing, Input Handling, UI Window Management, Rendering Pipeline, File Structure, Integration Point, Implementation Phases, Testing Strategy, HiDPI / Retina Display Support, and Alternatives Considered — describe the original custom-SDL2-bindings exploration that predated adopting `ink-native`. They are retained as historical design context — `ink-native` handles window, font, and input internally, so emoemu does not implement any of this bespoke SDL/TTF/ANSI-parsing code, and file/class names like `sdl-ui/`, `SdlUiRenderer`, or `SdlWindowManager` do not exist in the shipped code. Where a passage below asserts something as a current requirement that is no longer true (e.g. a stated dependency on SDL2_ttf), treat this note as the correction.
|
|
10
|
+
|
|
11
|
+
**Key principle:** UI rendering mode follows game rendering mode. When a user selects native rendering (`--native` or `video_driver = "native"`), both the UI and game render to the native window. When a user selects terminal rendering modes (kitty, terminal, ascii, emoji), both UI and game render to the terminal. There is no separate UI mode selection.
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
### Goals
|
|
16
|
+
|
|
17
|
+
1. **Unified Experience Per Mode**: UI rendering follows game rendering mode - native mode renders everything in the native window, terminal modes render everything in terminal
|
|
18
|
+
2. **Consistent Visuals**: UI and game share the same output target, eliminating context switches
|
|
19
|
+
3. **Seamless Transitions**: No terminal/window split when using native mode
|
|
20
|
+
4. **Preserve Terminal Mode**: Terminal rendering modes (kitty, terminal, ascii, emoji) continue to work exactly as today
|
|
21
|
+
|
|
22
|
+
### Non-Goals
|
|
23
|
+
|
|
24
|
+
1. Separate UI renderer selection - UI mode always matches game renderer mode
|
|
25
|
+
2. Complex GUI toolkit features (drag-and-drop, advanced animations)
|
|
26
|
+
3. Custom widget rendering (reuse Ink's component model where possible)
|
|
27
|
+
4. GPU-accelerated UI rendering
|
|
28
|
+
|
|
29
|
+
### Background: Pre-`ink-native` Architecture
|
|
30
|
+
|
|
31
|
+
Before this TRD's design shipped, the (now-removed) `ink-sdl` architecture separated UI and game rendering when SDL mode was selected:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌─────────────────────────────────────┐
|
|
35
|
+
│ Terminal (Ink.js/React) │
|
|
36
|
+
│ ├── ROM Browser │
|
|
37
|
+
│ ├── Settings Panel │
|
|
38
|
+
│ ├── Core Selector │
|
|
39
|
+
│ └── Dialogs │
|
|
40
|
+
└─────────────────────────────────────┘
|
|
41
|
+
↓ (user selects ROM)
|
|
42
|
+
stdin reset
|
|
43
|
+
↓
|
|
44
|
+
┌─────────────────────────────────────┐
|
|
45
|
+
│ Native Window (SDL Renderer) │
|
|
46
|
+
│ ├── Game Output │
|
|
47
|
+
│ └── Status Bar │
|
|
48
|
+
└─────────────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Pain points (SDL mode only):**
|
|
52
|
+
|
|
53
|
+
| Issue | Impact |
|
|
54
|
+
|-------|--------|
|
|
55
|
+
| **Context switching** | User sees terminal → SDL window → terminal transitions |
|
|
56
|
+
| **Visual inconsistency** | Terminal UI looks different from SDL game display |
|
|
57
|
+
| **Input mode changes** | Keyboard handling differs between terminal (Kitty protocol) and SDL |
|
|
58
|
+
| **Window management** | Two separate contexts to manage |
|
|
59
|
+
|
|
60
|
+
**Note:** Terminal modes (kitty, terminal, ascii, emoji) don't have this problem - everything renders to the terminal consistently. This TRD addresses the native/SDL mode experience only.
|
|
61
|
+
|
|
62
|
+
### Target Architecture (Shipped, via `ink-native`)
|
|
63
|
+
|
|
64
|
+
The render mode selection determines both UI and game output:
|
|
65
|
+
|
|
66
|
+
**Terminal Modes (kitty, terminal, ascii, emoji):**
|
|
67
|
+
```
|
|
68
|
+
┌─────────────────────────────────────┐
|
|
69
|
+
│ Terminal (unified) │
|
|
70
|
+
│ ├── ROM Browser (Ink) │
|
|
71
|
+
│ ├── Game Output (selected mode) │
|
|
72
|
+
│ └── Status Bar │
|
|
73
|
+
└─────────────────────────────────────┘
|
|
74
|
+
(no change from today)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Native Mode:**
|
|
78
|
+
```
|
|
79
|
+
┌─────────────────────────────────────────┐
|
|
80
|
+
│ Native Window (unified) │
|
|
81
|
+
│ ├── ROM Browser (Ink, via ink-native) │
|
|
82
|
+
│ ├── Game Output (framebuffer blit) │
|
|
83
|
+
│ └── Status Bar │
|
|
84
|
+
└─────────────────────────────────────────┘
|
|
85
|
+
(unified experience - shipped)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Architecture
|
|
91
|
+
|
|
92
|
+
### Ink Rendering Model
|
|
93
|
+
|
|
94
|
+
Ink uses a custom renderer built on React's reconciler. Key concepts:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Ink's render pipeline
|
|
98
|
+
React Component → Ink Reconciler → Output (yoga layout) → Terminal Output
|
|
99
|
+
|
|
100
|
+
// Terminal output generates ANSI escape sequences
|
|
101
|
+
// Written to stdout via ink's internal render loop
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Key files in Ink:**
|
|
105
|
+
|
|
106
|
+
- `ink/src/render.ts` - Reconciler and render loop
|
|
107
|
+
- `ink/src/output.ts` - Terminal output generation
|
|
108
|
+
- `ink/src/components/` - Box, Text, and other primitives
|
|
109
|
+
|
|
110
|
+
### Proposed Solution: Custom Ink Output Target
|
|
111
|
+
|
|
112
|
+
Create a custom output target that renders to an SDL texture instead of generating ANSI sequences:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
React Component → Ink Reconciler → Yoga Layout → SDL Texture Output
|
|
116
|
+
↓
|
|
117
|
+
SDL Window Display
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### High-Level Architecture
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// New rendering pipeline
|
|
124
|
+
interface SdlUiRenderer {
|
|
125
|
+
// Receive layout from Ink
|
|
126
|
+
renderFrame(nodes: InkNode[]): void;
|
|
127
|
+
|
|
128
|
+
// Text rendering
|
|
129
|
+
drawText(text: string, x: number, y: number, style: TextStyle): void;
|
|
130
|
+
|
|
131
|
+
// Box rendering
|
|
132
|
+
drawBox(x: number, y: number, width: number, height: number, style: BoxStyle): void;
|
|
133
|
+
|
|
134
|
+
// Present to screen
|
|
135
|
+
present(): void;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Implementation Approaches
|
|
142
|
+
|
|
143
|
+
### Option A: Fork Ink's Output Layer
|
|
144
|
+
|
|
145
|
+
Modify Ink's output generation to target SDL instead of terminal:
|
|
146
|
+
|
|
147
|
+
**Pros:**
|
|
148
|
+
- Maintains full Ink component compatibility
|
|
149
|
+
- Existing UI code works unchanged
|
|
150
|
+
- React devtools and debugging still work
|
|
151
|
+
|
|
152
|
+
**Cons:**
|
|
153
|
+
- Requires deep understanding of Ink internals
|
|
154
|
+
- May break with Ink updates
|
|
155
|
+
- Complex integration with yoga layout
|
|
156
|
+
|
|
157
|
+
### Option B: Custom React Renderer for SDL
|
|
158
|
+
|
|
159
|
+
Build a new React reconciler that renders directly to SDL:
|
|
160
|
+
|
|
161
|
+
**Pros:**
|
|
162
|
+
- Clean separation from Ink
|
|
163
|
+
- Full control over rendering pipeline
|
|
164
|
+
- Can optimize for SDL-specific features
|
|
165
|
+
|
|
166
|
+
**Cons:**
|
|
167
|
+
- Significant implementation effort
|
|
168
|
+
- Must reimplement component primitives (Box, Text, etc.)
|
|
169
|
+
- Duplicates much of Ink's work
|
|
170
|
+
|
|
171
|
+
### Option C: Ink Output Interception (Recommended)
|
|
172
|
+
|
|
173
|
+
Intercept Ink's rendered output and convert to SDL rendering commands:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Intercept Ink's stdout writes
|
|
177
|
+
const inkOutputStream = new InkToSdlStream(sdlRenderer);
|
|
178
|
+
render(<App />, { stdout: inkOutputStream });
|
|
179
|
+
|
|
180
|
+
class InkToSdlStream extends Writable {
|
|
181
|
+
write(chunk: Buffer): boolean {
|
|
182
|
+
const text = chunk.toString();
|
|
183
|
+
// Parse ANSI sequences and convert to SDL draw calls
|
|
184
|
+
this.parseAndRender(text);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Pros:**
|
|
191
|
+
- Minimal changes to existing Ink code
|
|
192
|
+
- Works with current UI components
|
|
193
|
+
- Fallback to terminal is trivial (just use real stdout)
|
|
194
|
+
|
|
195
|
+
**Cons:**
|
|
196
|
+
- ANSI parsing adds complexity
|
|
197
|
+
- Some terminal features may not map cleanly to SDL
|
|
198
|
+
- Character-based positioning requires font metrics
|
|
199
|
+
|
|
200
|
+
**Recommendation**: Option C provides the best balance of effort vs. compatibility.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Text Rendering
|
|
205
|
+
|
|
206
|
+
### Font Requirements
|
|
207
|
+
|
|
208
|
+
| Requirement | Rationale |
|
|
209
|
+
|-------------|-----------|
|
|
210
|
+
| **Monospace** | Ink layouts assume fixed-width characters |
|
|
211
|
+
| **Unicode support** | Box-drawing characters, symbols |
|
|
212
|
+
| **HiDPI support** | Crisp rendering at any scale factor |
|
|
213
|
+
| **Consistent metrics** | Layout must match Ink's character grid |
|
|
214
|
+
|
|
215
|
+
### Approach Comparison
|
|
216
|
+
|
|
217
|
+
| Factor | TTF (SDL_ttf) | Bitmap Atlas |
|
|
218
|
+
|--------|---------------|--------------|
|
|
219
|
+
| **HiDPI handling** | Excellent - render at exact physical size | Requires multiple atlases (1x, 2x, 3x) |
|
|
220
|
+
| **Fractional scaling** | Native support (Windows 125%, 150%) | Must round to nearest integer |
|
|
221
|
+
| **Display changes** | Re-open font at new size | Swap atlas, may cause visual jump |
|
|
222
|
+
| **Unicode coverage** | Full (depends on font) | Limited to pre-rendered glyphs |
|
|
223
|
+
| **Render performance** | Moderate (glyph caching helps) | Fast (simple texture blits) |
|
|
224
|
+
| **Asset management** | Single .ttf file | Multiple .png atlases |
|
|
225
|
+
| **Aesthetic** | Modern, smooth | Retro, pixel-perfect |
|
|
226
|
+
|
|
227
|
+
### Recommendation: TTF with Pixel Font (Updated)
|
|
228
|
+
|
|
229
|
+
> **Shipped reality:** `ink-native` uses a fixed, embedded **Cozette bitmap font** — not TTF. There is no runtime font selection or re-rasterization; the font is baked into the package and scales via the fixed `scaleFactor` set at window creation. The TTF analysis below was the pre-`ink-native` exploration and does not describe the shipped font pipeline.
|
|
230
|
+
|
|
231
|
+
Given HiDPI is a core requirement, **TTF is recommended** for the following reasons:
|
|
232
|
+
|
|
233
|
+
1. **Single asset** - One .ttf file vs. maintaining 3+ bitmap atlases
|
|
234
|
+
2. **Fractional scaling** - Windows 125%/150% scaling works without rounding hacks
|
|
235
|
+
3. **Display migration** - Moving window between 1x and 2x displays is seamless
|
|
236
|
+
4. **Unicode** - Full box-drawing, symbols, and international character support
|
|
237
|
+
|
|
238
|
+
To preserve the retro aesthetic, use a **pixel/bitmap-style TTF font**:
|
|
239
|
+
- [Cozette](https://github.com/slavfox/Cozette) - Bitmap-style TTF, good Unicode coverage
|
|
240
|
+
- [Monocraft](https://github.com/IdreesInc/Monocraft) - Minecraft-inspired, TTF format
|
|
241
|
+
- [PixelMplus](https://github.com/itouhiro/PixelMplus) - Japanese pixel font with Latin
|
|
242
|
+
- [Ark Pixel](https://github.com/TakWolf/ark-pixel-font) - Pan-CJK pixel font
|
|
243
|
+
|
|
244
|
+
These fonts render with a retro look but scale cleanly via TTF.
|
|
245
|
+
|
|
246
|
+
### SDL_ttf Implementation
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// src/rendering/sdl-ui/text-renderer.ts
|
|
250
|
+
|
|
251
|
+
interface SdlTextRenderer {
|
|
252
|
+
private font: Pointer;
|
|
253
|
+
private fontSize: number;
|
|
254
|
+
private scaleFactor: number;
|
|
255
|
+
private glyphCache: Map<string, SDL_Texture>;
|
|
256
|
+
|
|
257
|
+
constructor(fontPath: string, baseSize: number, scaleFactor: number) {
|
|
258
|
+
this.fontSize = baseSize;
|
|
259
|
+
this.scaleFactor = scaleFactor;
|
|
260
|
+
// Open font at physical size for crisp rendering
|
|
261
|
+
const physicalSize = Math.round(baseSize * scaleFactor);
|
|
262
|
+
this.font = TTF_OpenFont(fontPath, physicalSize);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Re-open font when scale factor changes (display change)
|
|
266
|
+
updateScaleFactor(newScale: number): void {
|
|
267
|
+
if (newScale !== this.scaleFactor) {
|
|
268
|
+
TTF_CloseFont(this.font);
|
|
269
|
+
this.scaleFactor = newScale;
|
|
270
|
+
const physicalSize = Math.round(this.fontSize * newScale);
|
|
271
|
+
this.font = TTF_OpenFont(this.fontPath, physicalSize);
|
|
272
|
+
this.glyphCache.clear(); // Invalidate cached glyphs
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
measureText(text: string): { width: number; height: number } {
|
|
277
|
+
const w = Buffer.alloc(4);
|
|
278
|
+
const h = Buffer.alloc(4);
|
|
279
|
+
TTF_SizeUTF8(this.font, text, w, h);
|
|
280
|
+
return { width: w.readInt32LE(0), height: h.readInt32LE(0) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
renderText(text: string, color: Color): SDL_Texture {
|
|
284
|
+
// Check cache first
|
|
285
|
+
const cacheKey = `${text}:${color.r}:${color.g}:${color.b}`;
|
|
286
|
+
if (this.glyphCache.has(cacheKey)) {
|
|
287
|
+
return this.glyphCache.get(cacheKey)!;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const surface = TTF_RenderUTF8_Blended(this.font, text, color);
|
|
291
|
+
const texture = SDL_CreateTextureFromSurface(this.renderer, surface);
|
|
292
|
+
SDL_FreeSurface(surface);
|
|
293
|
+
|
|
294
|
+
this.glyphCache.set(cacheKey, texture);
|
|
295
|
+
return texture;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Fallback: Bitmap Font (Optional)
|
|
301
|
+
|
|
302
|
+
For users who prefer pixel-perfect rendering at the cost of flexibility:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
interface BitmapFont {
|
|
306
|
+
textures: Map<'1x' | '2x' | '3x', SDL_Texture>;
|
|
307
|
+
glyphWidth: number; // Logical size (e.g., 8)
|
|
308
|
+
glyphHeight: number; // Logical size (e.g., 16)
|
|
309
|
+
|
|
310
|
+
getAtlasForScale(scale: number): { texture: SDL_Texture; physicalGlyphSize: number } {
|
|
311
|
+
const key = scale >= 2.5 ? '3x' : scale >= 1.5 ? '2x' : '1x';
|
|
312
|
+
const multiplier = key === '3x' ? 3 : key === '2x' ? 2 : 1;
|
|
313
|
+
return {
|
|
314
|
+
texture: this.textures.get(key)!,
|
|
315
|
+
physicalGlyphSize: this.glyphWidth * multiplier,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Can be offered as a config option: `sdl_ui_font_type = "ttf" | "bitmap"`
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## SDL Bindings Extensions
|
|
326
|
+
|
|
327
|
+
### New SDL2 Functions Required
|
|
328
|
+
|
|
329
|
+
Add to `src/rendering/sdl-bindings.ts`:
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
// Text rendering (SDL_ttf)
|
|
333
|
+
TTF_Init(): number;
|
|
334
|
+
TTF_Quit(): void;
|
|
335
|
+
TTF_OpenFont(file: string, ptsize: number): Pointer;
|
|
336
|
+
TTF_CloseFont(font: Pointer): void;
|
|
337
|
+
TTF_RenderUTF8_Blended(font: Pointer, text: string, color: SDL_Color): Pointer;
|
|
338
|
+
TTF_SizeUTF8(font: Pointer, text: string, w: Pointer, h: Pointer): number;
|
|
339
|
+
|
|
340
|
+
// Additional texture operations
|
|
341
|
+
SDL_CreateTextureFromSurface(renderer: Pointer, surface: Pointer): Pointer;
|
|
342
|
+
SDL_FreeSurface(surface: Pointer): void;
|
|
343
|
+
SDL_SetTextureBlendMode(texture: Pointer, blendMode: number): number;
|
|
344
|
+
SDL_SetRenderDrawColor(renderer: Pointer, r: number, g: number, b: number, a: number): number;
|
|
345
|
+
SDL_RenderFillRect(renderer: Pointer, rect: Pointer): number;
|
|
346
|
+
SDL_RenderDrawRect(renderer: Pointer, rect: Pointer): number;
|
|
347
|
+
|
|
348
|
+
// HiDPI support
|
|
349
|
+
SDL_GetRendererOutputSize(renderer: Pointer, w: Pointer, h: Pointer): number;
|
|
350
|
+
SDL_GetWindowDisplayIndex(window: Pointer): number;
|
|
351
|
+
SDL_GetDisplayDPI(displayIndex: number, ddpi: Pointer, hdpi: Pointer, vdpi: Pointer): number;
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### SDL Window Flags
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Window creation flags for HiDPI
|
|
358
|
+
const SDL_WINDOW_ALLOW_HIGHDPI = 0x00002000;
|
|
359
|
+
const SDL_WINDOW_RESIZABLE = 0x00000020;
|
|
360
|
+
|
|
361
|
+
// Window event types for display changes
|
|
362
|
+
const SDL_WINDOWEVENT_DISPLAY_CHANGED = 20;
|
|
363
|
+
const SDL_WINDOWEVENT_SIZE_CHANGED = 6;
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## ANSI Sequence Parsing
|
|
369
|
+
|
|
370
|
+
### Sequences to Support
|
|
371
|
+
|
|
372
|
+
| Sequence | Purpose | SDL Equivalent |
|
|
373
|
+
|----------|---------|----------------|
|
|
374
|
+
| `\x1b[{n};{m}H` | Cursor position | Set draw coordinates |
|
|
375
|
+
| `\x1b[{n}m` | SGR (colors, styles) | Set text/fill color |
|
|
376
|
+
| `\x1b[38;2;{r};{g};{b}m` | 24-bit foreground | RGB text color |
|
|
377
|
+
| `\x1b[48;2;{r};{g};{b}m` | 24-bit background | RGB fill color |
|
|
378
|
+
| `\x1b[1m` | Bold | Bold font variant or brighter color |
|
|
379
|
+
| `\x1b[4m` | Underline | Draw underline |
|
|
380
|
+
| `\x1b[7m` | Reverse video | Swap fg/bg colors |
|
|
381
|
+
| `\x1b[2J` | Clear screen | Clear texture |
|
|
382
|
+
|
|
383
|
+
### Parser Implementation
|
|
384
|
+
|
|
385
|
+
```typescript
|
|
386
|
+
// src/rendering/ansi-parser.ts
|
|
387
|
+
|
|
388
|
+
interface DrawCommand {
|
|
389
|
+
type: 'text' | 'fill' | 'clear' | 'cursor';
|
|
390
|
+
// ... command-specific data
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const parseAnsiStream = (input: string): DrawCommand[] => {
|
|
394
|
+
const commands: DrawCommand[] = [];
|
|
395
|
+
let cursor = { x: 0, y: 0 };
|
|
396
|
+
let style = { fg: WHITE, bg: BLACK, bold: false, underline: false };
|
|
397
|
+
|
|
398
|
+
// State machine to parse escape sequences and text
|
|
399
|
+
// ...
|
|
400
|
+
|
|
401
|
+
return commands;
|
|
402
|
+
};
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Input Handling
|
|
408
|
+
|
|
409
|
+
### Keyboard Input
|
|
410
|
+
|
|
411
|
+
When UI renders to SDL, keyboard input comes from SDL events instead of stdin:
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
// Map SDL key events to Ink input
|
|
415
|
+
const sdlKeyToInkKey = (event: SDL_KeyboardEvent): string | null => {
|
|
416
|
+
switch (event.keysym.sym) {
|
|
417
|
+
case SDLK_UP: return 'up';
|
|
418
|
+
case SDLK_DOWN: return 'down';
|
|
419
|
+
case SDLK_LEFT: return 'left';
|
|
420
|
+
case SDLK_RIGHT: return 'right';
|
|
421
|
+
case SDLK_RETURN: return 'return';
|
|
422
|
+
case SDLK_ESCAPE: return 'escape';
|
|
423
|
+
case SDLK_TAB: return 'tab';
|
|
424
|
+
case SDLK_BACKSPACE: return 'backspace';
|
|
425
|
+
default:
|
|
426
|
+
// Handle printable characters
|
|
427
|
+
if (event.keysym.sym >= 32 && event.keysym.sym < 127) {
|
|
428
|
+
return String.fromCharCode(event.keysym.sym);
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Mouse Input
|
|
436
|
+
|
|
437
|
+
Ink supports mouse via terminal mouse protocols. For SDL:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// SDL mouse events
|
|
441
|
+
SDL_MOUSEMOTION → track hover state
|
|
442
|
+
SDL_MOUSEBUTTONDOWN → click events
|
|
443
|
+
SDL_MOUSEWHEEL → scroll events
|
|
444
|
+
|
|
445
|
+
// Convert pixel coordinates to character grid
|
|
446
|
+
const pixelToChar = (x: number, y: number): { col: number; row: number } => ({
|
|
447
|
+
col: Math.floor(x / glyphWidth),
|
|
448
|
+
row: Math.floor(y / glyphHeight),
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Input Injection
|
|
453
|
+
|
|
454
|
+
Create a fake stdin stream that receives SDL input:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
class SdlInputBridge extends Readable {
|
|
458
|
+
handleSdlEvent(event: SDL_Event): void {
|
|
459
|
+
if (event.type === SDL_KEYDOWN) {
|
|
460
|
+
const key = sdlKeyToInkKey(event.key);
|
|
461
|
+
if (key) {
|
|
462
|
+
// Push to Ink's input stream
|
|
463
|
+
this.push(key);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## UI Window Management
|
|
473
|
+
|
|
474
|
+
This section applies to SDL mode only. Terminal modes continue to use the existing Ink-based UI with no changes.
|
|
475
|
+
|
|
476
|
+
### Mode Selection Flow
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
Application Start
|
|
480
|
+
↓
|
|
481
|
+
┌──────────────────────────────────────┐
|
|
482
|
+
│ Check video_driver setting │
|
|
483
|
+
└──────────────────────────────────────┘
|
|
484
|
+
↓ ↓
|
|
485
|
+
SDL mode Terminal mode
|
|
486
|
+
↓ ↓
|
|
487
|
+
┌──────────────┐ ┌──────────────────┐
|
|
488
|
+
│ SDL Window │ │ Terminal (Ink) │
|
|
489
|
+
│ UI + Game │ │ UI + Game │
|
|
490
|
+
└──────────────┘ └──────────────────┘
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### SDL Mode Window Lifecycle
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
Application Start (SDL mode)
|
|
497
|
+
↓
|
|
498
|
+
┌──────────────────────────┐
|
|
499
|
+
│ SDL UI Window Created │
|
|
500
|
+
│ - ROM Browser renders │
|
|
501
|
+
│ - Settings accessible │
|
|
502
|
+
└──────────────────────────┘
|
|
503
|
+
↓ (user launches game)
|
|
504
|
+
┌──────────────────────────┐
|
|
505
|
+
│ Same Window, New Mode │
|
|
506
|
+
│ - Game renders │
|
|
507
|
+
│ - UI overlay (optional) │
|
|
508
|
+
└──────────────────────────┘
|
|
509
|
+
↓ (user exits game)
|
|
510
|
+
┌──────────────────────────┐
|
|
511
|
+
│ Return to UI Mode │
|
|
512
|
+
│ - ROM Browser renders │
|
|
513
|
+
└──────────────────────────┘
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Unified Window vs. Separate Windows
|
|
517
|
+
|
|
518
|
+
**Option 1: Single Window (Recommended)**
|
|
519
|
+
|
|
520
|
+
One SDL window used for both UI and game:
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
class UnifiedSdlWindow {
|
|
524
|
+
private mode: 'ui' | 'game' = 'ui';
|
|
525
|
+
|
|
526
|
+
setMode(mode: 'ui' | 'game'): void {
|
|
527
|
+
this.mode = mode;
|
|
528
|
+
if (mode === 'ui') {
|
|
529
|
+
this.resize(UI_WIDTH, UI_HEIGHT);
|
|
530
|
+
} else {
|
|
531
|
+
this.resize(gameWidth * scale, gameHeight * scale);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Pros:** Seamless transitions, consistent window position
|
|
538
|
+
**Cons:** Window resizes between modes
|
|
539
|
+
|
|
540
|
+
**Option 2: Separate Windows**
|
|
541
|
+
|
|
542
|
+
UI window and game window are independent:
|
|
543
|
+
|
|
544
|
+
**Pros:** No resizing needed, can show both simultaneously
|
|
545
|
+
**Cons:** More complex window management, less integrated experience
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Rendering Pipeline
|
|
550
|
+
|
|
551
|
+
### Frame Composition
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
class SdlUiRenderer {
|
|
555
|
+
private texture: SDL_Texture; // UI render target
|
|
556
|
+
private dirtyRegions: SDL_Rect[] = [];
|
|
557
|
+
|
|
558
|
+
beginFrame(): void {
|
|
559
|
+
SDL_SetRenderTarget(this.renderer, this.texture);
|
|
560
|
+
SDL_SetRenderDrawColor(this.renderer, 0, 0, 0, 255);
|
|
561
|
+
SDL_RenderClear(this.renderer);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
drawText(text: string, x: number, y: number, style: TextStyle): void {
|
|
565
|
+
// Render text to texture at character grid position
|
|
566
|
+
const pixelX = x * this.glyphWidth;
|
|
567
|
+
const pixelY = y * this.glyphHeight;
|
|
568
|
+
|
|
569
|
+
// Draw background if set
|
|
570
|
+
if (style.bg) {
|
|
571
|
+
this.fillRect(pixelX, pixelY, text.length * this.glyphWidth, this.glyphHeight, style.bg);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Draw text
|
|
575
|
+
this.renderTextToTexture(text, pixelX, pixelY, style.fg);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
endFrame(): void {
|
|
579
|
+
SDL_SetRenderTarget(this.renderer, null);
|
|
580
|
+
SDL_RenderCopy(this.renderer, this.texture, null, null);
|
|
581
|
+
SDL_RenderPresent(this.renderer);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Dirty Region Optimization
|
|
587
|
+
|
|
588
|
+
Only re-render changed portions:
|
|
589
|
+
|
|
590
|
+
```typescript
|
|
591
|
+
// Track which character cells changed
|
|
592
|
+
interface DirtyTracker {
|
|
593
|
+
markDirty(col: number, row: number, width: number, height: number): void;
|
|
594
|
+
getDirtyRegions(): SDL_Rect[];
|
|
595
|
+
clear(): void;
|
|
596
|
+
}
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Component Compatibility
|
|
602
|
+
|
|
603
|
+
### Ink Components That Work Unchanged
|
|
604
|
+
|
|
605
|
+
| Component | Notes |
|
|
606
|
+
|-----------|-------|
|
|
607
|
+
| `Box` | Layout via yoga, renders as colored rectangles |
|
|
608
|
+
| `Text` | Direct text rendering |
|
|
609
|
+
| `Newline` | Cursor positioning |
|
|
610
|
+
| `Spacer` | Layout spacing |
|
|
611
|
+
|
|
612
|
+
### Components Requiring Adaptation
|
|
613
|
+
|
|
614
|
+
| Component | Challenge | Solution |
|
|
615
|
+
|-----------|-----------|----------|
|
|
616
|
+
| `Static` | Scrollback buffer | Maintain virtual buffer |
|
|
617
|
+
| `TextInput` | Cursor blinking | SDL timer for cursor |
|
|
618
|
+
| Thumbnails (custom) | Kitty graphics protocol | Render directly to SDL |
|
|
619
|
+
|
|
620
|
+
### Thumbnail Rendering
|
|
621
|
+
|
|
622
|
+
Current thumbnails use Kitty graphics protocol. For SDL:
|
|
623
|
+
|
|
624
|
+
```typescript
|
|
625
|
+
// Load thumbnail image directly
|
|
626
|
+
const thumbnail = SDL_LoadBMP(thumbnailPath); // or PNG via SDL_image
|
|
627
|
+
const texture = SDL_CreateTextureFromSurface(renderer, thumbnail);
|
|
628
|
+
|
|
629
|
+
// Render at grid position
|
|
630
|
+
SDL_RenderCopy(renderer, texture, null, destRect);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
This actually simplifies thumbnail handling since SDL can render images directly.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Configuration
|
|
638
|
+
|
|
639
|
+
### Behavior by Render Mode
|
|
640
|
+
|
|
641
|
+
UI rendering automatically matches the selected game render mode:
|
|
642
|
+
|
|
643
|
+
| Render Mode | Game Output | UI Output | Input Source |
|
|
644
|
+
|-------------|-------------|-----------|--------------|
|
|
645
|
+
| `native` | Native window (fenster) | Native window (fenster) | ink-native keyboard events |
|
|
646
|
+
| `kitty` | Terminal (Kitty protocol) | Terminal (Ink) | Terminal stdin |
|
|
647
|
+
| `terminal` | Terminal (half-blocks) | Terminal (Ink) | Terminal stdin |
|
|
648
|
+
| `ascii` | Terminal (ASCII) | Terminal (Ink) | Terminal stdin |
|
|
649
|
+
| `emoji` | Terminal (emoji) | Terminal (Ink) | Terminal stdin |
|
|
650
|
+
|
|
651
|
+
No separate configuration needed - selecting `--native` or `video_driver = "native"` automatically enables native UI rendering.
|
|
652
|
+
|
|
653
|
+
### Config Keys (Shipped)
|
|
654
|
+
|
|
655
|
+
Unlike the `sdl_ui_font` / `sdl_ui_font_size` / `sdl_ui_scale` keys originally proposed below, no new config keys shipped. The font is fixed (bundled Cozette, not configurable) and UI/game scale reuses the existing `menu_scale_factor` key, passed to `ink-native` as `scaleFactor`:
|
|
656
|
+
|
|
657
|
+
| Key | Type | Default | Description |
|
|
658
|
+
|-----|------|---------|-------------|
|
|
659
|
+
| `menu_scale_factor` | number | auto | UI/window scale, passed to `ink-native` as `scaleFactor`. Applies only at window creation - no runtime change. |
|
|
660
|
+
|
|
661
|
+
### CLI Usage
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
# Native mode: both UI and game render to the native window
|
|
665
|
+
emoemu --native
|
|
666
|
+
|
|
667
|
+
# Terminal modes: both UI and game render to terminal (unchanged behavior)
|
|
668
|
+
emoemu --kitty
|
|
669
|
+
emoemu --terminal
|
|
670
|
+
emoemu --ascii
|
|
671
|
+
emoemu --emoji
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## File Structure
|
|
677
|
+
|
|
678
|
+
```
|
|
679
|
+
src/rendering/
|
|
680
|
+
├── sdl-renderer.ts # Existing: Game rendering
|
|
681
|
+
├── sdl-bindings.ts # Existing: SDL2 FFI (extend with TTF)
|
|
682
|
+
├── sdl-ui/
|
|
683
|
+
│ ├── index.ts # SdlUiRenderer main class
|
|
684
|
+
│ ├── ansi-parser.ts # ANSI escape sequence parser
|
|
685
|
+
│ ├── text-renderer.ts # Text rendering (bitmap or TTF)
|
|
686
|
+
│ ├── input-bridge.ts # SDL input → Ink input translation
|
|
687
|
+
│ ├── bitmap-font.ts # Bitmap font atlas handling
|
|
688
|
+
│ └── fonts/
|
|
689
|
+
│ └── default.png # Built-in bitmap font atlas
|
|
690
|
+
|
|
691
|
+
src/ui/
|
|
692
|
+
├── App.tsx # Existing: Entry point (no changes needed)
|
|
693
|
+
├── RomBrowser/ # Existing: ROM browser (no changes needed)
|
|
694
|
+
└── ... # Other UI components (no changes needed)
|
|
695
|
+
|
|
696
|
+
src/
|
|
697
|
+
├── index.ts # Modified: Route to SDL or terminal UI based on render mode
|
|
698
|
+
└── Emulator/ # Existing: Game rendering (minimal changes)
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## Integration Point
|
|
704
|
+
|
|
705
|
+
### Routing UI to SDL or Terminal
|
|
706
|
+
|
|
707
|
+
The main entry point (`src/index.ts`) determines which UI path to use based on the configured render mode:
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
// In src/index.ts - launchBrowser or equivalent
|
|
711
|
+
|
|
712
|
+
const renderMode = getRenderMode(); // From config or CLI
|
|
713
|
+
|
|
714
|
+
if (renderMode === 'sdl') {
|
|
715
|
+
// SDL mode: Create SDL window for UI
|
|
716
|
+
const sdlUi = new SdlUiRenderer({
|
|
717
|
+
width: UI_WIDTH,
|
|
718
|
+
height: UI_HEIGHT,
|
|
719
|
+
font: config.sdl_ui_font,
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Create custom streams that route to SDL
|
|
723
|
+
const sdlOutputStream = new SdlOutputStream(sdlUi);
|
|
724
|
+
const sdlInputStream = new SdlInputStream(sdlUi);
|
|
725
|
+
|
|
726
|
+
// Render Ink to SDL
|
|
727
|
+
const { waitUntilExit } = render(<App />, {
|
|
728
|
+
stdout: sdlOutputStream,
|
|
729
|
+
stdin: sdlInputStream,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
await waitUntilExit();
|
|
733
|
+
} else {
|
|
734
|
+
// Terminal mode: Use existing Ink setup (unchanged)
|
|
735
|
+
const { waitUntilExit } = render(<App />);
|
|
736
|
+
await waitUntilExit();
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Shared SDL Window Between UI and Game
|
|
741
|
+
|
|
742
|
+
When transitioning from UI to game in SDL mode, reuse the same window:
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
class SdlWindowManager {
|
|
746
|
+
private window: Pointer;
|
|
747
|
+
private renderer: Pointer;
|
|
748
|
+
|
|
749
|
+
// UI mode
|
|
750
|
+
getUiRenderer(): SdlUiRenderer {
|
|
751
|
+
return new SdlUiRenderer({ window: this.window, renderer: this.renderer });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Game mode
|
|
755
|
+
getGameRenderer(width: number, height: number): SdlRenderer {
|
|
756
|
+
return new SdlRenderer({
|
|
757
|
+
window: this.window,
|
|
758
|
+
renderer: this.renderer,
|
|
759
|
+
width,
|
|
760
|
+
height,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## Implementation Phases
|
|
769
|
+
|
|
770
|
+
### Phase 1: Foundation
|
|
771
|
+
|
|
772
|
+
1. Create `sdl-ui/` directory structure
|
|
773
|
+
2. Add SDL_ttf bindings to `sdl-bindings.ts`
|
|
774
|
+
3. Implement ANSI escape sequence parser
|
|
775
|
+
4. Create TTF text renderer with scale-aware font sizing
|
|
776
|
+
5. HiDPI window creation with `SDL_WINDOW_ALLOW_HIGHDPI`
|
|
777
|
+
6. Scale factor detection and coordinate scaling
|
|
778
|
+
7. Bundle Cozette TTF font
|
|
779
|
+
|
|
780
|
+
**Deliverable:** Can render crisp plain text to SDL window on both standard and HiDPI displays
|
|
781
|
+
|
|
782
|
+
### Phase 2: Ink Integration
|
|
783
|
+
|
|
784
|
+
1. Create output stream interceptor (`SdlOutputStream`)
|
|
785
|
+
2. Wire Ink render to SDL output
|
|
786
|
+
3. Implement cursor positioning
|
|
787
|
+
4. Add color support (SGR sequences)
|
|
788
|
+
|
|
789
|
+
**Deliverable:** Ink components render to SDL (text-only, no interaction)
|
|
790
|
+
|
|
791
|
+
### Phase 3: Input Handling
|
|
792
|
+
|
|
793
|
+
1. Implement SDL keyboard → Ink input bridge (`SdlInputStream`)
|
|
794
|
+
2. Add mouse support
|
|
795
|
+
3. Handle focus and window events
|
|
796
|
+
|
|
797
|
+
**Deliverable:** Interactive UI in SDL window
|
|
798
|
+
|
|
799
|
+
### Phase 4: Mode Routing
|
|
800
|
+
|
|
801
|
+
1. Add render mode check in `src/index.ts`
|
|
802
|
+
2. Create `SdlWindowManager` for shared window between UI and game
|
|
803
|
+
3. Implement UI → game → UI transitions in SDL mode
|
|
804
|
+
4. Verify terminal modes remain unchanged
|
|
805
|
+
|
|
806
|
+
**Deliverable:** `--native` flag gives unified native-window experience; other modes unchanged
|
|
807
|
+
|
|
808
|
+
### Phase 5: Visual Polish
|
|
809
|
+
|
|
810
|
+
1. Box-drawing character support
|
|
811
|
+
2. Bold/underline text styles
|
|
812
|
+
3. Thumbnail rendering (direct SDL image loading)
|
|
813
|
+
4. Consistent styling between UI and game status bar
|
|
814
|
+
|
|
815
|
+
**Deliverable:** Feature parity with terminal UI
|
|
816
|
+
|
|
817
|
+
### Phase 6: Optimization
|
|
818
|
+
|
|
819
|
+
1. Dirty region tracking
|
|
820
|
+
2. Texture caching for repeated text
|
|
821
|
+
3. Glyph atlas optimization
|
|
822
|
+
|
|
823
|
+
**Deliverable:** Smooth 60 FPS UI rendering
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Testing Strategy
|
|
828
|
+
|
|
829
|
+
### Unit Tests
|
|
830
|
+
|
|
831
|
+
| Test | Description |
|
|
832
|
+
|------|-------------|
|
|
833
|
+
| ANSI parser | Verify correct parsing of all supported sequences |
|
|
834
|
+
| Text measurement | Font metrics match expected character grid |
|
|
835
|
+
| Input translation | SDL keys map correctly to Ink input |
|
|
836
|
+
| Color conversion | SGR codes produce correct RGB values |
|
|
837
|
+
|
|
838
|
+
### Integration Tests
|
|
839
|
+
|
|
840
|
+
| Test | Description |
|
|
841
|
+
|------|-------------|
|
|
842
|
+
| Basic render | Simple Ink app renders to SDL |
|
|
843
|
+
| Layout | Box/flex layouts match terminal rendering |
|
|
844
|
+
| Interaction | Keyboard navigation works in SDL mode |
|
|
845
|
+
| Mode switch | UI → game → UI transitions work cleanly |
|
|
846
|
+
|
|
847
|
+
### Mode-Specific Tests
|
|
848
|
+
|
|
849
|
+
Verify both modes work independently:
|
|
850
|
+
|
|
851
|
+
| Mode | Test | Expected Behavior |
|
|
852
|
+
|------|------|-------------------|
|
|
853
|
+
| Native | Launch with `--native` | Native window shows ROM browser |
|
|
854
|
+
| SDL | Select game | Same window transitions to game |
|
|
855
|
+
| SDL | Exit game | Same window returns to ROM browser |
|
|
856
|
+
| Kitty | Launch with `--kitty` | Terminal shows ROM browser (unchanged) |
|
|
857
|
+
| Kitty | Select game | Terminal shows game (unchanged) |
|
|
858
|
+
| Terminal | Launch with `--terminal` | Terminal shows ROM browser (unchanged) |
|
|
859
|
+
| Terminal | Select game | Terminal shows game (unchanged) |
|
|
860
|
+
|
|
861
|
+
### Regression Tests
|
|
862
|
+
|
|
863
|
+
Ensure terminal modes are not affected:
|
|
864
|
+
|
|
865
|
+
| Test | Description |
|
|
866
|
+
|------|-------------|
|
|
867
|
+
| Kitty graphics | Thumbnails render correctly in Kitty mode |
|
|
868
|
+
| Mouse support | Terminal mouse (SGR 1006) still works |
|
|
869
|
+
| Keyboard input | Kitty keyboard protocol still works |
|
|
870
|
+
| Gamepad | Gamepad input works in both modes |
|
|
871
|
+
|
|
872
|
+
### Visual Tests
|
|
873
|
+
|
|
874
|
+
| Test | Expected Result |
|
|
875
|
+
|------|-----------------|
|
|
876
|
+
| ROM browser (SDL) | Grid layout, thumbnails, selection highlight |
|
|
877
|
+
| ROM browser (terminal) | Same layout, Kitty/Unicode thumbnails |
|
|
878
|
+
| Settings panel | Form inputs, toggles, dropdowns |
|
|
879
|
+
| Dialogs | Modal overlays, button focus |
|
|
880
|
+
| Search | Text input with cursor |
|
|
881
|
+
|
|
882
|
+
### Performance Tests
|
|
883
|
+
|
|
884
|
+
| Metric | Target |
|
|
885
|
+
|--------|--------|
|
|
886
|
+
| UI frame time (SDL) | < 16ms (60 FPS) |
|
|
887
|
+
| UI frame time (terminal) | Unchanged from current |
|
|
888
|
+
| Input latency | < 50ms |
|
|
889
|
+
| Memory (SDL UI mode) | < 50MB additional |
|
|
890
|
+
|
|
891
|
+
### HiDPI Tests
|
|
892
|
+
|
|
893
|
+
| Test Case | Display | Expected Result |
|
|
894
|
+
|-----------|---------|-----------------|
|
|
895
|
+
| Text rendering | macOS Retina 2x | Crisp, no blur or fuzziness |
|
|
896
|
+
| Text rendering | macOS 1x | Normal, sharp text |
|
|
897
|
+
| Text rendering | Windows 150% | Crisp (renders at 2x) |
|
|
898
|
+
| Font atlas selection | 2x display | Uses 2x bitmap atlas |
|
|
899
|
+
| Window move | 1x → 2x display | Re-renders at new scale, stays crisp |
|
|
900
|
+
| Window resize | Any | Maintains crisp text at all sizes |
|
|
901
|
+
| Thumbnails | 2x display | Sharp image rendering |
|
|
902
|
+
|
|
903
|
+
---
|
|
904
|
+
|
|
905
|
+
## Dependencies
|
|
906
|
+
|
|
907
|
+
### Required (Shipped)
|
|
908
|
+
|
|
909
|
+
| Dependency | Purpose |
|
|
910
|
+
|------------|---------|
|
|
911
|
+
| `ink-native` | npm package: bundled `fenster` native window backend + embedded Cozette bitmap font. **No system dependencies** (no SDL2, no SDL2_ttf). |
|
|
912
|
+
| Ink | React-based UI (already required) |
|
|
913
|
+
|
|
914
|
+
There is no "Optional" dependency tier - `ink-native` bundles everything needed for window, font, and thumbnail image display; no equivalent of SDL2_image is required.
|
|
915
|
+
|
|
916
|
+
### Font Asset (Shipped)
|
|
917
|
+
|
|
918
|
+
`ink-native` embeds the **Cozette** bitmap font directly in the package - no separate font file to bundle or select:
|
|
919
|
+
|
|
920
|
+
| Font | Style | Unicode | License |
|
|
921
|
+
|------|-------|---------|---------|
|
|
922
|
+
| [Cozette](https://github.com/slavfox/Cozette) | Bitmap, embedded in `ink-native` | Good (box-drawing, symbols) | MIT |
|
|
923
|
+
|
|
924
|
+
The Monocraft/JetBrains Mono alternatives considered below were not adopted - the font is fixed by `ink-native`, not user-selectable.
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## HiDPI / Retina Display Support
|
|
929
|
+
|
|
930
|
+
Crisp UI rendering on HiDPI displays (macOS Retina, Windows high-DPI, Linux HiDPI) is essential. Blurry text is unacceptable.
|
|
931
|
+
|
|
932
|
+
### The HiDPI Challenge
|
|
933
|
+
|
|
934
|
+
On a 2x Retina display, a "1280x720" window actually has 2560x1440 physical pixels. Without proper handling:
|
|
935
|
+
- Text rendered at logical resolution gets scaled up → blurry
|
|
936
|
+
- Bitmap fonts look pixelated
|
|
937
|
+
- UI appears fuzzy compared to native apps
|
|
938
|
+
|
|
939
|
+
### SDL HiDPI Architecture
|
|
940
|
+
|
|
941
|
+
```
|
|
942
|
+
┌─────────────────────────────────────────────────────┐
|
|
943
|
+
│ Logical Size (what we request): 1280 x 720 │
|
|
944
|
+
│ Physical Size (actual pixels): 2560 x 1440 │
|
|
945
|
+
│ Scale Factor: 2x │
|
|
946
|
+
└─────────────────────────────────────────────────────┘
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Implementation Requirements
|
|
950
|
+
|
|
951
|
+
#### 1. Window Creation with HiDPI Flag
|
|
952
|
+
|
|
953
|
+
```typescript
|
|
954
|
+
// Enable HiDPI support when creating the window
|
|
955
|
+
const window = SDL_CreateWindow(
|
|
956
|
+
title,
|
|
957
|
+
SDL_WINDOWPOS_CENTERED,
|
|
958
|
+
SDL_WINDOWPOS_CENTERED,
|
|
959
|
+
logicalWidth,
|
|
960
|
+
logicalHeight,
|
|
961
|
+
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE
|
|
962
|
+
);
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
#### 2. Query Actual Drawable Size
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
// Get the real pixel dimensions (not logical)
|
|
969
|
+
const getDrawableSize = (window: Pointer): { width: number; height: number } => {
|
|
970
|
+
const w = Buffer.alloc(4);
|
|
971
|
+
const h = Buffer.alloc(4);
|
|
972
|
+
SDL_GetRendererOutputSize(renderer, w, h);
|
|
973
|
+
return {
|
|
974
|
+
width: w.readInt32LE(0),
|
|
975
|
+
height: h.readInt32LE(0),
|
|
976
|
+
};
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
// Calculate scale factor
|
|
980
|
+
const logical = { width: 1280, height: 720 };
|
|
981
|
+
const physical = getDrawableSize(window);
|
|
982
|
+
const scaleFactor = physical.width / logical.width; // 2.0 on Retina
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
#### 3. Render at Native Resolution
|
|
986
|
+
|
|
987
|
+
All rendering must happen at physical pixel resolution:
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
class SdlUiRenderer {
|
|
991
|
+
private scaleFactor: number;
|
|
992
|
+
private physicalWidth: number;
|
|
993
|
+
private physicalHeight: number;
|
|
994
|
+
|
|
995
|
+
constructor(options: SdlUiRendererOptions) {
|
|
996
|
+
// Create window at logical size
|
|
997
|
+
this.window = SDL_CreateWindow(..., logicalWidth, logicalHeight, SDL_WINDOW_ALLOW_HIGHDPI);
|
|
998
|
+
|
|
999
|
+
// Get actual pixel dimensions
|
|
1000
|
+
const physical = this.getDrawableSize();
|
|
1001
|
+
this.physicalWidth = physical.width;
|
|
1002
|
+
this.physicalHeight = physical.height;
|
|
1003
|
+
this.scaleFactor = physical.width / logicalWidth;
|
|
1004
|
+
|
|
1005
|
+
// Create texture at PHYSICAL size for crisp rendering
|
|
1006
|
+
this.texture = SDL_CreateTexture(
|
|
1007
|
+
this.renderer,
|
|
1008
|
+
SDL_PIXELFORMAT_RGBA8888,
|
|
1009
|
+
SDL_TEXTUREACCESS_TARGET,
|
|
1010
|
+
this.physicalWidth, // Physical pixels, not logical
|
|
1011
|
+
this.physicalHeight
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
#### 4. Scale-Aware Font Rendering
|
|
1018
|
+
|
|
1019
|
+
TTF fonts handle HiDPI naturally by rendering at the physical pixel size:
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
// Scale font size by display factor
|
|
1023
|
+
const baseFontSize = 16; // Logical size (what we'd use at 1x)
|
|
1024
|
+
const physicalFontSize = Math.round(baseFontSize * scaleFactor); // 32 on 2x Retina
|
|
1025
|
+
const font = TTF_OpenFont(fontPath, physicalFontSize);
|
|
1026
|
+
|
|
1027
|
+
// Font renders at native resolution - no scaling artifacts
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
This is the primary advantage of TTF over bitmap fonts for HiDPI - a single font file works at any scale factor without maintaining multiple assets.
|
|
1031
|
+
|
|
1032
|
+
#### 5. Scale-Aware Coordinate System
|
|
1033
|
+
|
|
1034
|
+
All Ink coordinates (character grid) must be scaled:
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
// Convert character position to physical pixels
|
|
1038
|
+
const charToPhysical = (col: number, row: number): { x: number; y: number } => ({
|
|
1039
|
+
x: col * glyphWidth * scaleFactor,
|
|
1040
|
+
y: row * glyphHeight * scaleFactor,
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Drawing uses physical coordinates
|
|
1044
|
+
drawText(text: string, col: number, row: number, style: TextStyle): void {
|
|
1045
|
+
const { x, y } = this.charToPhysical(col, row);
|
|
1046
|
+
// Render at physical coordinates with scaled font
|
|
1047
|
+
this.renderTextAtPhysicalPos(text, x, y, style);
|
|
1048
|
+
}
|
|
1049
|
+
```
|
|
1050
|
+
|
|
1051
|
+
### Platform-Specific Considerations
|
|
1052
|
+
|
|
1053
|
+
| Platform | Scale Factors | Notes |
|
|
1054
|
+
|----------|---------------|-------|
|
|
1055
|
+
| macOS Retina | 2x (common), 1x | `SDL_WINDOW_ALLOW_HIGHDPI` required |
|
|
1056
|
+
| macOS Pro Display XDR | Up to 2x | Same handling |
|
|
1057
|
+
| Windows | 1x, 1.25x, 1.5x, 2x, etc. | Fractional scaling common |
|
|
1058
|
+
| Linux (Wayland) | Integer (1x, 2x) | Wayland handles scaling well |
|
|
1059
|
+
| Linux (X11) | Varies | May need `SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR=0` |
|
|
1060
|
+
|
|
1061
|
+
### Fractional Scaling
|
|
1062
|
+
|
|
1063
|
+
Windows commonly uses fractional scales (125%, 150%). Options:
|
|
1064
|
+
|
|
1065
|
+
**Option A: Round to nearest integer (Recommended)**
|
|
1066
|
+
```typescript
|
|
1067
|
+
const effectiveScale = Math.round(scaleFactor); // 1.5 → 2
|
|
1068
|
+
```
|
|
1069
|
+
Slightly larger UI but always crisp.
|
|
1070
|
+
|
|
1071
|
+
**Option B: Render at exact scale**
|
|
1072
|
+
```typescript
|
|
1073
|
+
const effectiveScale = scaleFactor; // 1.5 exactly
|
|
1074
|
+
```
|
|
1075
|
+
Requires careful sub-pixel rendering, may still have artifacts.
|
|
1076
|
+
|
|
1077
|
+
**Recommendation:** Round to nearest integer for guaranteed crispness.
|
|
1078
|
+
|
|
1079
|
+
### Testing HiDPI
|
|
1080
|
+
|
|
1081
|
+
| Test Case | Expected Result |
|
|
1082
|
+
|-----------|-----------------|
|
|
1083
|
+
| macOS Retina 2x | Crisp text, no blur |
|
|
1084
|
+
| macOS non-Retina 1x | Normal rendering |
|
|
1085
|
+
| Windows 150% scaling | Crisp (rounded to 2x) |
|
|
1086
|
+
| Windows 100% scaling | Normal rendering |
|
|
1087
|
+
| Linux HiDPI | Crisp text |
|
|
1088
|
+
| Mixed-DPI (move window between displays) | Re-query scale, re-render |
|
|
1089
|
+
|
|
1090
|
+
### Display Change Handling
|
|
1091
|
+
|
|
1092
|
+
Handle window moving between displays with different DPI:
|
|
1093
|
+
|
|
1094
|
+
```typescript
|
|
1095
|
+
// In SDL event loop
|
|
1096
|
+
if (event.type === SDL_WINDOWEVENT) {
|
|
1097
|
+
if (event.window.event === SDL_WINDOWEVENT_DISPLAY_CHANGED ||
|
|
1098
|
+
event.window.event === SDL_WINDOWEVENT_SIZE_CHANGED) {
|
|
1099
|
+
// Re-query drawable size - scale factor may have changed
|
|
1100
|
+
const newPhysical = this.getDrawableSize();
|
|
1101
|
+
if (newPhysical.width !== this.physicalWidth) {
|
|
1102
|
+
this.handleScaleChange(newPhysical);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
handleScaleChange(newPhysical: Size): void {
|
|
1108
|
+
this.physicalWidth = newPhysical.width;
|
|
1109
|
+
this.physicalHeight = newPhysical.height;
|
|
1110
|
+
this.scaleFactor = newPhysical.width / this.logicalWidth;
|
|
1111
|
+
|
|
1112
|
+
// Recreate texture at new size
|
|
1113
|
+
SDL_DestroyTexture(this.texture);
|
|
1114
|
+
this.texture = SDL_CreateTexture(..., this.physicalWidth, this.physicalHeight);
|
|
1115
|
+
|
|
1116
|
+
// Reload font at new size
|
|
1117
|
+
this.reloadFont();
|
|
1118
|
+
|
|
1119
|
+
// Force full redraw
|
|
1120
|
+
this.invalidateAll();
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### Config Options
|
|
1125
|
+
|
|
1126
|
+
| Key | Type | Default | Description |
|
|
1127
|
+
|-----|------|---------|-------------|
|
|
1128
|
+
| `sdl_ui_dpi_aware` | boolean | `true` | Enable HiDPI rendering |
|
|
1129
|
+
| `sdl_ui_dpi_rounding` | string | `"nearest"` | `"nearest"`, `"floor"`, `"ceil"` for fractional scaling |
|
|
1130
|
+
|
|
1131
|
+
---
|
|
1132
|
+
|
|
1133
|
+
## Limitations
|
|
1134
|
+
|
|
1135
|
+
**Native mode limitations (shipped):**
|
|
1136
|
+
|
|
1137
|
+
1. **Character grid constraint**: Native UI must match terminal's character-based model for Ink compatibility
|
|
1138
|
+
2. **No smooth animations**: Frame-based updates like terminal
|
|
1139
|
+
3. **Limited typography**: Single embedded font (Cozette), no custom fonts or styling
|
|
1140
|
+
4. **No rich text**: No inline images (except thumbnails in designated areas)
|
|
1141
|
+
5. **No runtime `setTitle`**: `ink-native` exposes no API to change the window title after creation
|
|
1142
|
+
6. **No runtime scale-factor change**: `menu_scale_factor` (→ ink-native's `scaleFactor`) only applies at window creation
|
|
1143
|
+
7. **No programmatic window resize**: the game letterboxes into the fixed window instead of resizing it
|
|
1144
|
+
|
|
1145
|
+
**General limitations:**
|
|
1146
|
+
|
|
1147
|
+
1. **No mixed modes**: Cannot use native window for game and terminal for UI (or vice versa)
|
|
1148
|
+
2. **Font dependency**: Native mode is limited to the font bundled with `ink-native` (Cozette) - no custom font support
|
|
1149
|
+
|
|
1150
|
+
---
|
|
1151
|
+
|
|
1152
|
+
## Future Enhancements
|
|
1153
|
+
|
|
1154
|
+
1. **Smooth scrolling**: Pixel-level scroll instead of line-based
|
|
1155
|
+
2. **Transitions**: Fade/slide animations between screens
|
|
1156
|
+
3. **Custom themes**: User-configurable colors and fonts
|
|
1157
|
+
4. **In-game overlay**: Pause menu, save state UI rendered over game
|
|
1158
|
+
5. **Sub-pixel text rendering**: LCD sub-pixel antialiasing for even crisper text
|
|
1159
|
+
|
|
1160
|
+
---
|
|
1161
|
+
|
|
1162
|
+
## Alternatives Considered
|
|
1163
|
+
|
|
1164
|
+
### Dear ImGui
|
|
1165
|
+
|
|
1166
|
+
Immediate-mode GUI library with SDL backend:
|
|
1167
|
+
|
|
1168
|
+
**Pros:** Battle-tested SDL integration, extensive widget library
|
|
1169
|
+
**Cons:** Would require rewriting all UI code, different paradigm than React
|
|
1170
|
+
|
|
1171
|
+
### Electron/WebView
|
|
1172
|
+
|
|
1173
|
+
Embed web browser for UI:
|
|
1174
|
+
|
|
1175
|
+
**Pros:** Full HTML/CSS capabilities, could reuse Ink with web target
|
|
1176
|
+
**Cons:** Heavy dependency, complex integration, performance overhead
|
|
1177
|
+
|
|
1178
|
+
### SDL_gui / Other SDL GUI Libraries
|
|
1179
|
+
|
|
1180
|
+
Various SDL GUI libraries exist:
|
|
1181
|
+
|
|
1182
|
+
**Pros:** Native SDL integration
|
|
1183
|
+
**Cons:** Most are unmaintained, would still require UI rewrite
|
|
1184
|
+
|
|
1185
|
+
**Conclusion:** Intercepting Ink output is the least invasive approach that preserves existing UI code while enabling SDL rendering.
|
|
1186
|
+
|
|
1187
|
+
---
|
|
1188
|
+
|
|
1189
|
+
## Resources
|
|
1190
|
+
|
|
1191
|
+
- [Ink Documentation](https://github.com/vadimdemedes/ink)
|
|
1192
|
+
- [Ink Reconciler Source](https://github.com/vadimdemedes/ink/blob/master/src/reconciler.ts)
|
|
1193
|
+
- [SDL_ttf Documentation](https://wiki.libsdl.org/SDL2_ttf/FrontPage)
|
|
1194
|
+
- [ANSI Escape Codes Reference](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
|
1195
|
+
- [Yoga Layout Engine](https://yogalayout.dev/)
|