Fixed HUD and menu position and scaling in hardware renderer

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 19:25:24 +01:00
parent c8cd2cb144
commit 3e091c3d5d
6 changed files with 136 additions and 53 deletions

View File

@@ -94,10 +94,15 @@ class _GameScreenState extends State<GameScreen> {
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<GameScreen> {
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<GameScreen> {
});
}
void _toggleFpsCounter() {
_engine.showFpsCounter = !_engine.showFpsCounter;
}
String _modeLabel(_RendererMode mode) {
switch (mode) {
case _RendererMode.software:

View File

@@ -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;

View File

@@ -79,7 +79,9 @@ abstract class RendererBackend<T>
if (engine.difficulty == null) {
drawMenu(engine);
drawFpsOverlay(engine);
if (engine.showFpsCounter) {
drawFpsOverlay(engine);
}
return finalizeFrame();
}
@@ -89,7 +91,9 @@ abstract class RendererBackend<T>
// 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();

View File

@@ -203,9 +203,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
);
}
if (parts.length > 1) {
_drawMenuText(
_drawCanonicalMenuText(
parts.sublist(1).join(' '),
textX,
y + 12,
@@ -292,8 +297,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
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<FrameBuffer> {
_blitVgaImage(cursor, panelX + 10, y - 2);
}
_drawMenuText(
_drawCanonicalMenuText(
Difficulty.values[i].title,
textX,
y,
@@ -453,6 +463,74 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
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<String> 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<FrameBuffer> {
}
}
/// 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<FrameBuffer> {
/// 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];

View File

@@ -26,8 +26,8 @@ class WolfGlslRenderer extends BaseWolfRenderer {
}
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
static const int _renderWidth = 320;
static const int _renderHeight = 200;
static const int _renderWidth = 640;
static const int _renderHeight = 400;
final SoftwareRenderer _renderer = SoftwareRenderer();

View File

@@ -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);
}