From 3e091c3d5dfdd3bcf54f90c6fc20036e8c04de5c Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Thu, 19 Mar 2026 19:25:24 +0100 Subject: [PATCH] Fixed HUD and menu position and scaling in hardware renderer Signed-off-by: Hans Kokx --- apps/wolf_3d_gui/lib/screens/game_screen.dart | 19 ++- .../lib/src/engine/wolf_3d_engine_base.dart | 3 + .../lib/src/rendering/renderer_backend.dart | 8 +- .../lib/src/rendering/software_renderer.dart | 148 +++++++++++++----- .../lib/wolf_3d_glsl_renderer.dart | 4 +- .../wolf_3d_renderer/shaders/wolf_world.frag | 7 +- 6 files changed, 136 insertions(+), 53 deletions(-) diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index f29b807..f7ffeda 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -94,10 +94,15 @@ class _GameScreenState extends State { Focus( autofocus: true, onKeyEvent: (node, event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.tab) { - setState(_cycleRendererMode); - return KeyEventResult.handled; + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.tab) { + setState(_cycleRendererMode); + return KeyEventResult.handled; + } + if (event.logicalKey == LogicalKeyboardKey.backquote) { + setState(_toggleFpsCounter); + return KeyEventResult.handled; + } } return KeyEventResult.ignored; }, @@ -118,7 +123,7 @@ class _GameScreenState extends State { top: 16, right: 16, child: Text( - 'TAB: ${_modeLabel(_rendererMode)}', + 'TAB: ${_modeLabel(_rendererMode)} `: FPS ${_engine.showFpsCounter ? 'On' : 'Off'}', style: TextStyle( color: Colors.white.withValues(alpha: 0.5), ), @@ -172,6 +177,10 @@ class _GameScreenState extends State { }); } + void _toggleFpsCounter() { + _engine.showFpsCounter = !_engine.showFpsCounter; + } + String _modeLabel(_RendererMode mode) { switch (mode) { case _RendererMode.software: diff --git a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart index 90353e5..869456e 100644 --- a/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_dart/lib/src/engine/wolf_3d_engine_base.dart @@ -80,6 +80,9 @@ class WolfEngine { /// Current smoothed FPS, suitable for lightweight on-screen diagnostics. double get fps => _smoothedFps; + /// Whether renderers should draw the FPS counter overlay. + bool showFpsCounter = true; + /// The episode index where the game session begins. final int? startingEpisode; diff --git a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart index 95a3ae5..705b32b 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/renderer_backend.dart @@ -79,7 +79,9 @@ abstract class RendererBackend if (engine.difficulty == null) { drawMenu(engine); - drawFpsOverlay(engine); + if (engine.showFpsCounter) { + drawFpsOverlay(engine); + } return finalizeFrame(); } @@ -89,7 +91,9 @@ abstract class RendererBackend // 3. Draw 2D overlays. drawWeapon(engine); drawHud(engine); - drawFpsOverlay(engine); + if (engine.showFpsCounter) { + drawFpsOverlay(engine); + } // 4. Finalize and return the frame data (Buffer or String/List). return finalizeFrame(); diff --git a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart index 5d601e5..d33e6e9 100644 --- a/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart +++ b/packages/wolf_3d_dart/lib/src/rendering/software_renderer.dart @@ -203,9 +203,9 @@ class SoftwareRenderer extends RendererBackend { const int panelY = 58; const int panelW = 264; const int panelH = 104; - _fillMenuPanel(panelX, panelY, panelW, panelH, panelColor); + _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor); - _drawMenuTextCentered('SELECT GAME', 38, headingColor, scale: 2); + _drawCanonicalMenuTextCentered('SELECT GAME', 38, headingColor, scale: 2); final cursor = art.mappedPic( engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8, @@ -222,7 +222,7 @@ class SoftwareRenderer extends RendererBackend { if (isSelected && cursor != null) { _blitVgaImage(cursor, panelX + 10, y - 2); } - _drawMenuText( + _drawCanonicalMenuText( _gameTitle(engine.availableGames[i].version), textX, y, @@ -243,9 +243,14 @@ class SoftwareRenderer extends RendererBackend { const int panelY = 20; const int panelW = 296; const int panelH = 158; - _fillMenuPanel(panelX, panelY, panelW, panelH, panelColor); + _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor); - _drawMenuTextCentered('WHICH EPISODE TO PLAY?', 6, headingColor, scale: 2); + _drawCanonicalMenuTextCentered( + 'WHICH EPISODE TO PLAY?', + 6, + headingColor, + scale: 2, + ); final cursor = art.mappedPic( engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8, @@ -271,7 +276,7 @@ class SoftwareRenderer extends RendererBackend { final parts = engine.data.episodes[i].name.split('\n'); if (parts.isNotEmpty) { - _drawMenuText( + _drawCanonicalMenuText( parts.first, textX, y + 1, @@ -279,7 +284,7 @@ class SoftwareRenderer extends RendererBackend { ); } if (parts.length > 1) { - _drawMenuText( + _drawCanonicalMenuText( parts.sublist(1).join(' '), textX, y + 12, @@ -292,8 +297,8 @@ class SoftwareRenderer extends RendererBackend { void _drawCenteredMenuFooter(WolfClassicMenuArt art) { final bottom = art.mappedPic(15); if (bottom != null) { - final int x = ((width - bottom.width) ~/ 2).clamp(0, width - 1); - final int y = (height - bottom.height - 8).clamp(0, height - 1); + final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319); + final int y = (200 - bottom.height - 8).clamp(0, 199); _blitVgaImage(bottom, x, y); return; } @@ -316,9 +321,9 @@ class SoftwareRenderer extends RendererBackend { textWidth += WolfMenuFont.measureTextWidth(text, 1); } - final int panelWidth = (textWidth + 12).clamp(1, width); - final int panelX = ((width - panelWidth) ~/ 2).clamp(0, width - 1); - _fillMenuPanel( + final int panelWidth = (textWidth + 12).clamp(1, 320); + final int panelX = ((320 - panelWidth) ~/ 2).clamp(0, 319); + _fillCanonicalRect( panelX, _menuFooterY, panelWidth, @@ -329,7 +334,7 @@ class SoftwareRenderer extends RendererBackend { int cursorX = panelX + 6; const int textY = _menuFooterY + 2; for (final (text, color) in segments) { - _drawMenuText(text, cursorX, textY, color, scale: 1); + _drawCanonicalMenuText(text, cursorX, textY, color, scale: 1); cursorX += WolfMenuFont.measureTextWidth(text, 1); } } @@ -348,14 +353,19 @@ class SoftwareRenderer extends RendererBackend { const int panelY = 70; const int panelW = 264; const int panelH = 82; - _fillMenuPanel(panelX, panelY, panelW, panelH, panelColor); + _fillCanonicalRect(panelX, panelY, panelW, panelH, panelColor); - _drawMenuTextCentered(Difficulty.menuText, 48, headingColor, scale: 2); + _drawCanonicalMenuTextCentered( + Difficulty.menuText, + 48, + headingColor, + scale: 2, + ); final bottom = art.mappedPic(15); if (bottom != null) { - final x = (width - bottom.width) ~/ 2; - final y = height - bottom.height - 8; + final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319); + final int y = (200 - bottom.height - 8).clamp(0, 199); _blitVgaImage(bottom, x, y); } @@ -380,7 +390,7 @@ class SoftwareRenderer extends RendererBackend { _blitVgaImage(cursor, panelX + 10, y - 2); } - _drawMenuText( + _drawCanonicalMenuText( Difficulty.values[i].title, textX, y, @@ -453,6 +463,74 @@ class SoftwareRenderer extends RendererBackend { return (0xFF000000) | (b << 16) | (g << 8) | r; } + double get _uiScaleX => width / 320.0; + + double get _uiScaleY => height / 200.0; + + void _fillCanonicalRect( + int startX320, + int startY200, + int width320, + int height200, + int color, + ) { + final int startX = (startX320 * _uiScaleX).floor(); + final int endX = ((startX320 + width320) * _uiScaleX).ceil(); + final int startY = (startY200 * _uiScaleY).floor(); + final int endY = ((startY200 + height200) * _uiScaleY).ceil(); + + for (int y = startY; y < endY; y++) { + if (y < 0 || y >= height) continue; + final int rowStart = y * width; + for (int x = startX; x < endX; x++) { + if (x >= 0 && x < width) { + _buffer.pixels[rowStart + x] = color; + } + } + } + } + + void _drawCanonicalMenuText( + String text, + int startX320, + int startY200, + int color, { + int scale = 1, + }) { + int x320 = startX320; + for (final rune in text.runes) { + final String char = String.fromCharCode(rune).toUpperCase(); + final List pattern = WolfMenuFont.glyphFor(char); + + for (int row = 0; row < pattern.length; row++) { + final String bits = pattern[row]; + for (int col = 0; col < bits.length; col++) { + if (bits[col] != '1') continue; + _fillCanonicalRect( + x320 + (col * scale), + startY200 + (row * scale), + scale, + scale, + color, + ); + } + } + + x320 += WolfMenuFont.glyphAdvance(char, scale); + } + } + + void _drawCanonicalMenuTextCentered( + String text, + int y200, + int color, { + int scale = 1, + }) { + final int textWidth = WolfMenuFont.measureTextWidth(text, scale); + final int x320 = ((320 - textWidth) ~/ 2).clamp(0, 319); + _drawCanonicalMenuText(text, x320, y200, color, scale: scale); + } + /// Draws bitmap menu text directly into the framebuffer. void _drawMenuText( String text, @@ -486,18 +564,6 @@ class SoftwareRenderer extends RendererBackend { } } - /// Draws bitmap menu text centered in the current framebuffer width. - void _drawMenuTextCentered( - String text, - int y, - int color, { - int scale = 1, - }) { - final int textWidth = WolfMenuFont.measureTextWidth(text, scale); - final int x = ((width - textWidth) ~/ 2).clamp(0, width - 1); - _drawMenuText(text, x, y, color, scale: scale); - } - @override FrameBuffer finalizeFrame() { // If the player took damage, overlay a red tint across the 3D view @@ -513,17 +579,23 @@ class SoftwareRenderer extends RendererBackend { /// Maps planar VGA image data directly to 32-bit framebuffer pixels. /// - /// This renderer assumes a 1:1 mapping with the canonical 320x200 layout. + /// UI coordinates are expressed in canonical 320x200 space and scaled to the + /// current framebuffer so higher-resolution render targets preserve layout. void _blitVgaImage(VgaImage image, int startX, int startY) { - for (int dy = 0; dy < image.height; dy++) { - for (int dx = 0; dx < image.width; dx++) { - int drawX = startX + dx; - int drawY = startY + dy; + final int destStartX = (startX * _uiScaleX).floor(); + final int destStartY = (startY * _uiScaleY).floor(); + final int destWidth = math.max(1, (image.width * _uiScaleX).ceil()); + final int destHeight = math.max(1, (image.height * _uiScaleY).ceil()); + + for (int dy = 0; dy < destHeight; dy++) { + for (int dx = 0; dx < destWidth; dx++) { + final int drawX = destStartX + dx; + final int drawY = destStartY + dy; if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) { - int srcX = dx.clamp(0, image.width - 1); - int srcY = dy.clamp(0, image.height - 1); - int colorByte = image.decodePixel(srcX, srcY); + final int srcX = (dx / _uiScaleX).toInt().clamp(0, image.width - 1); + final int srcY = (dy / _uiScaleY).toInt().clamp(0, image.height - 1); + final int colorByte = image.decodePixel(srcX, srcY); if (colorByte != 255) { _buffer.pixels[drawY * width + drawX] = ColorPalette.vga32Bit[colorByte]; diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart index 3ee865a..3da0f16 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart @@ -26,8 +26,8 @@ class WolfGlslRenderer extends BaseWolfRenderer { } class _WolfGlslRendererState extends BaseWolfRendererState { - static const int _renderWidth = 320; - static const int _renderHeight = 200; + static const int _renderWidth = 640; + static const int _renderHeight = 400; final SoftwareRenderer _renderer = SoftwareRenderer(); diff --git a/packages/wolf_3d_renderer/shaders/wolf_world.frag b/packages/wolf_3d_renderer/shaders/wolf_world.frag index 84c1f64..d95e6de 100644 --- a/packages/wolf_3d_renderer/shaders/wolf_world.frag +++ b/packages/wolf_3d_renderer/shaders/wolf_world.frag @@ -34,10 +34,5 @@ void main() { vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25; vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45); - vec2 centered = uv - 0.5; - float vignette = 1.0 - dot(centered, centered) * 0.35; - vignette = clamp(vignette, 0.75, 1.0); - - vec3 color = aaColor * vignette; - fragColor = vec4(color, centerSample.a); + fragColor = vec4(aaColor, centerSample.a); }