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:
@@ -94,10 +94,15 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
Focus(
|
Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onKeyEvent: (node, event) {
|
onKeyEvent: (node, event) {
|
||||||
if (event is KeyDownEvent &&
|
if (event is KeyDownEvent) {
|
||||||
event.logicalKey == LogicalKeyboardKey.tab) {
|
if (event.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
setState(_cycleRendererMode);
|
setState(_cycleRendererMode);
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.backquote) {
|
||||||
|
setState(_toggleFpsCounter);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
},
|
},
|
||||||
@@ -118,7 +123,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
top: 16,
|
top: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: Text(
|
child: Text(
|
||||||
'TAB: ${_modeLabel(_rendererMode)}',
|
'TAB: ${_modeLabel(_rendererMode)} `: FPS ${_engine.showFpsCounter ? 'On' : 'Off'}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white.withValues(alpha: 0.5),
|
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) {
|
String _modeLabel(_RendererMode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case _RendererMode.software:
|
case _RendererMode.software:
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class WolfEngine {
|
|||||||
/// Current smoothed FPS, suitable for lightweight on-screen diagnostics.
|
/// Current smoothed FPS, suitable for lightweight on-screen diagnostics.
|
||||||
double get fps => _smoothedFps;
|
double get fps => _smoothedFps;
|
||||||
|
|
||||||
|
/// Whether renderers should draw the FPS counter overlay.
|
||||||
|
bool showFpsCounter = true;
|
||||||
|
|
||||||
/// The episode index where the game session begins.
|
/// The episode index where the game session begins.
|
||||||
final int? startingEpisode;
|
final int? startingEpisode;
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ abstract class RendererBackend<T>
|
|||||||
|
|
||||||
if (engine.difficulty == null) {
|
if (engine.difficulty == null) {
|
||||||
drawMenu(engine);
|
drawMenu(engine);
|
||||||
drawFpsOverlay(engine);
|
if (engine.showFpsCounter) {
|
||||||
|
drawFpsOverlay(engine);
|
||||||
|
}
|
||||||
return finalizeFrame();
|
return finalizeFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +91,9 @@ abstract class RendererBackend<T>
|
|||||||
// 3. Draw 2D overlays.
|
// 3. Draw 2D overlays.
|
||||||
drawWeapon(engine);
|
drawWeapon(engine);
|
||||||
drawHud(engine);
|
drawHud(engine);
|
||||||
drawFpsOverlay(engine);
|
if (engine.showFpsCounter) {
|
||||||
|
drawFpsOverlay(engine);
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Finalize and return the frame data (Buffer or String/List).
|
// 4. Finalize and return the frame data (Buffer or String/List).
|
||||||
return finalizeFrame();
|
return finalizeFrame();
|
||||||
|
|||||||
@@ -203,9 +203,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
const int panelY = 58;
|
const int panelY = 58;
|
||||||
const int panelW = 264;
|
const int panelW = 264;
|
||||||
const int panelH = 104;
|
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(
|
final cursor = art.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
||||||
@@ -222,7 +222,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
if (isSelected && cursor != null) {
|
if (isSelected && cursor != null) {
|
||||||
_blitVgaImage(cursor, panelX + 10, y - 2);
|
_blitVgaImage(cursor, panelX + 10, y - 2);
|
||||||
}
|
}
|
||||||
_drawMenuText(
|
_drawCanonicalMenuText(
|
||||||
_gameTitle(engine.availableGames[i].version),
|
_gameTitle(engine.availableGames[i].version),
|
||||||
textX,
|
textX,
|
||||||
y,
|
y,
|
||||||
@@ -243,9 +243,14 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
const int panelY = 20;
|
const int panelY = 20;
|
||||||
const int panelW = 296;
|
const int panelW = 296;
|
||||||
const int panelH = 158;
|
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(
|
final cursor = art.mappedPic(
|
||||||
engine.menuManager.isCursorAltFrame(engine.timeAliveMs) ? 9 : 8,
|
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');
|
final parts = engine.data.episodes[i].name.split('\n');
|
||||||
if (parts.isNotEmpty) {
|
if (parts.isNotEmpty) {
|
||||||
_drawMenuText(
|
_drawCanonicalMenuText(
|
||||||
parts.first,
|
parts.first,
|
||||||
textX,
|
textX,
|
||||||
y + 1,
|
y + 1,
|
||||||
@@ -279,7 +284,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
_drawMenuText(
|
_drawCanonicalMenuText(
|
||||||
parts.sublist(1).join(' '),
|
parts.sublist(1).join(' '),
|
||||||
textX,
|
textX,
|
||||||
y + 12,
|
y + 12,
|
||||||
@@ -292,8 +297,8 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
|
void _drawCenteredMenuFooter(WolfClassicMenuArt art) {
|
||||||
final bottom = art.mappedPic(15);
|
final bottom = art.mappedPic(15);
|
||||||
if (bottom != null) {
|
if (bottom != null) {
|
||||||
final int x = ((width - bottom.width) ~/ 2).clamp(0, width - 1);
|
final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319);
|
||||||
final int y = (height - bottom.height - 8).clamp(0, height - 1);
|
final int y = (200 - bottom.height - 8).clamp(0, 199);
|
||||||
_blitVgaImage(bottom, x, y);
|
_blitVgaImage(bottom, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -316,9 +321,9 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
textWidth += WolfMenuFont.measureTextWidth(text, 1);
|
textWidth += WolfMenuFont.measureTextWidth(text, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int panelWidth = (textWidth + 12).clamp(1, width);
|
final int panelWidth = (textWidth + 12).clamp(1, 320);
|
||||||
final int panelX = ((width - panelWidth) ~/ 2).clamp(0, width - 1);
|
final int panelX = ((320 - panelWidth) ~/ 2).clamp(0, 319);
|
||||||
_fillMenuPanel(
|
_fillCanonicalRect(
|
||||||
panelX,
|
panelX,
|
||||||
_menuFooterY,
|
_menuFooterY,
|
||||||
panelWidth,
|
panelWidth,
|
||||||
@@ -329,7 +334,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
int cursorX = panelX + 6;
|
int cursorX = panelX + 6;
|
||||||
const int textY = _menuFooterY + 2;
|
const int textY = _menuFooterY + 2;
|
||||||
for (final (text, color) in segments) {
|
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);
|
cursorX += WolfMenuFont.measureTextWidth(text, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,14 +353,19 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
const int panelY = 70;
|
const int panelY = 70;
|
||||||
const int panelW = 264;
|
const int panelW = 264;
|
||||||
const int panelH = 82;
|
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);
|
final bottom = art.mappedPic(15);
|
||||||
if (bottom != null) {
|
if (bottom != null) {
|
||||||
final x = (width - bottom.width) ~/ 2;
|
final int x = ((320 - bottom.width) ~/ 2).clamp(0, 319);
|
||||||
final y = height - bottom.height - 8;
|
final int y = (200 - bottom.height - 8).clamp(0, 199);
|
||||||
_blitVgaImage(bottom, x, y);
|
_blitVgaImage(bottom, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +390,7 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
_blitVgaImage(cursor, panelX + 10, y - 2);
|
_blitVgaImage(cursor, panelX + 10, y - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
_drawMenuText(
|
_drawCanonicalMenuText(
|
||||||
Difficulty.values[i].title,
|
Difficulty.values[i].title,
|
||||||
textX,
|
textX,
|
||||||
y,
|
y,
|
||||||
@@ -453,6 +463,74 @@ class SoftwareRenderer extends RendererBackend<FrameBuffer> {
|
|||||||
return (0xFF000000) | (b << 16) | (g << 8) | r;
|
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.
|
/// Draws bitmap menu text directly into the framebuffer.
|
||||||
void _drawMenuText(
|
void _drawMenuText(
|
||||||
String text,
|
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
|
@override
|
||||||
FrameBuffer finalizeFrame() {
|
FrameBuffer finalizeFrame() {
|
||||||
// If the player took damage, overlay a red tint across the 3D view
|
// 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.
|
/// 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) {
|
void _blitVgaImage(VgaImage image, int startX, int startY) {
|
||||||
for (int dy = 0; dy < image.height; dy++) {
|
final int destStartX = (startX * _uiScaleX).floor();
|
||||||
for (int dx = 0; dx < image.width; dx++) {
|
final int destStartY = (startY * _uiScaleY).floor();
|
||||||
int drawX = startX + dx;
|
final int destWidth = math.max(1, (image.width * _uiScaleX).ceil());
|
||||||
int drawY = startY + dy;
|
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) {
|
if (drawX >= 0 && drawX < width && drawY >= 0 && drawY < height) {
|
||||||
int srcX = dx.clamp(0, image.width - 1);
|
final int srcX = (dx / _uiScaleX).toInt().clamp(0, image.width - 1);
|
||||||
int srcY = dy.clamp(0, image.height - 1);
|
final int srcY = (dy / _uiScaleY).toInt().clamp(0, image.height - 1);
|
||||||
int colorByte = image.decodePixel(srcX, srcY);
|
final int colorByte = image.decodePixel(srcX, srcY);
|
||||||
if (colorByte != 255) {
|
if (colorByte != 255) {
|
||||||
_buffer.pixels[drawY * width + drawX] =
|
_buffer.pixels[drawY * width + drawX] =
|
||||||
ColorPalette.vga32Bit[colorByte];
|
ColorPalette.vga32Bit[colorByte];
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ class WolfGlslRenderer extends BaseWolfRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
||||||
static const int _renderWidth = 320;
|
static const int _renderWidth = 640;
|
||||||
static const int _renderHeight = 200;
|
static const int _renderHeight = 400;
|
||||||
|
|
||||||
final SoftwareRenderer _renderer = SoftwareRenderer();
|
final SoftwareRenderer _renderer = SoftwareRenderer();
|
||||||
|
|
||||||
|
|||||||
@@ -34,10 +34,5 @@ void main() {
|
|||||||
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
vec3 neighborhoodAvg = (sampleN + sampleS + sampleE + sampleW) * 0.25;
|
||||||
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
vec3 aaColor = mix(centerSample.rgb, neighborhoodAvg, edgeAmount * 0.45);
|
||||||
|
|
||||||
vec2 centered = uv - 0.5;
|
fragColor = vec4(aaColor, centerSample.a);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user