Improves ASCII rasterization speed and simplifies API

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-18 01:37:04 +01:00
parent 309bf5c699
commit d7692ea325
11 changed files with 120 additions and 47 deletions

View File

@@ -38,9 +38,9 @@ void main() async {
final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true); final AsciiRasterizer asciiRasterizer = AsciiRasterizer(isTerminal: true);
final SixelRasterizer sixelRasterizer = SixelRasterizer(); final SixelRasterizer sixelRasterizer = SixelRasterizer();
Rasterizer rasterizer = sixelRasterizer; CliRasterizer rasterizer = sixelRasterizer;
FrameBuffer buffer = FrameBuffer( final FrameBuffer initialFrameBuffer = FrameBuffer(
stdout.terminalColumns, stdout.terminalColumns,
stdout.terminalLines, stdout.terminalLines,
); );
@@ -49,6 +49,7 @@ void main() async {
data: availableGames.values.first, data: availableGames.values.first,
difficulty: Difficulty.medium, difficulty: Difficulty.medium,
startingEpisode: 0, startingEpisode: 0,
frameBuffer: initialFrameBuffer,
audio: CliSilentAudio(), audio: CliSilentAudio(),
input: CliInput(), input: CliInput(),
onGameWon: () { onGameWon: () {
@@ -83,24 +84,17 @@ void main() async {
if (stdout.hasTerminal) { if (stdout.hasTerminal) {
int cols = stdout.terminalColumns; int cols = stdout.terminalColumns;
int rows = stdout.terminalLines; int rows = stdout.terminalLines;
if (!rasterizer.isTerminalSizeSupported(cols, rows)) { if (!rasterizer.prepareTerminalFrame(engine, columns: cols, rows: rows)) {
// Clear the screen and print the warning at the top left // Clear the screen and print the warning at the top left
stdout.write('\x1b[2J\x1b[H'); stdout.write('\x1b[2J\x1b[H');
stdout.write('\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n');
stdout.write('${rasterizer.terminalSizeRequirement}\n');
stdout.write( stdout.write(
'Current size: \x1b[33m${stdout.terminalColumns}x${stdout.terminalLines}\x1b[0m\n\n', rasterizer.buildTerminalSizeWarning(columns: cols, rows: rows),
); );
stdout.write('Please resize your window to resume the game...');
// Prevent the engine from simulating a massive time jump when resumed // Prevent the engine from simulating a massive time jump when resumed
lastTick = stopwatch.elapsed; lastTick = stopwatch.elapsed;
return; return;
} }
if (buffer.width != cols || buffer.height != rows) {
buffer = FrameBuffer(cols, rows);
}
} }
// 2. Normal Game Loop // 2. Normal Game Loop
@@ -112,8 +106,6 @@ void main() async {
stdout.write('\x1b[H'); stdout.write('\x1b[H');
engine.tick(elapsed); engine.tick(elapsed);
rasterizer.render(engine, buffer); stdout.write(rasterizer.render(engine));
stdout.write(rasterizer.finalizeFrame());
}); });
} }

View File

@@ -37,6 +37,7 @@ class _GameScreenState extends State<GameScreen> {
data: widget.data, data: widget.data,
difficulty: widget.difficulty, difficulty: widget.difficulty,
startingEpisode: widget.startingEpisode, startingEpisode: widget.startingEpisode,
frameBuffer: FrameBuffer(320, 200),
audio: widget.audio, audio: widget.audio,
input: widget.input, input: widget.input,
onGameWon: () => Navigator.of(context).pop(), onGameWon: () => Navigator.of(context).pop(),

View File

@@ -59,7 +59,7 @@ class ColoredChar {
int get argb => (0xFF000000) | (r << 16) | (g << 8) | b; int get argb => (0xFF000000) | (r << 16) | (g << 8) | b;
} }
class AsciiRasterizer extends Rasterizer { class AsciiRasterizer extends CliRasterizer<dynamic> {
static const double _targetAspectRatio = 4 / 3; static const double _targetAspectRatio = 4 / 3;
static const int _terminalBackdropArgb = 0xFF009688; static const int _terminalBackdropArgb = 0xFF009688;
static const int _minimumTerminalColumns = 80; static const int _minimumTerminalColumns = 80;
@@ -121,14 +121,16 @@ class AsciiRasterizer extends Rasterizer {
// Intercept the base render call to initialize our text grid // Intercept the base render call to initialize our text grid
@override @override
dynamic render(WolfEngine engine, FrameBuffer buffer) { dynamic render(WolfEngine engine) {
_engine = engine; _engine = engine;
_screen = List.generate( _screen = List.generate(
buffer.height, engine.frameBuffer.height,
(_) => (_) => List.filled(
List.filled(buffer.width, ColoredChar(' ', ColorPalette.vga32Bit[0])), engine.frameBuffer.width,
ColoredChar(' ', ColorPalette.vga32Bit[0]),
),
); );
return super.render(engine, buffer); return super.render(engine);
} }
@override @override
@@ -630,7 +632,7 @@ class AsciiRasterizer extends Rasterizer {
} }
@override @override
String finalizeFrame() { dynamic finalizeFrame() {
if (_engine.player.damageFlash > 0.0) { if (_engine.player.damageFlash > 0.0) {
if (isTerminal) { if (isTerminal) {
_applyDamageFlashToScene(); _applyDamageFlashToScene();
@@ -640,9 +642,10 @@ class AsciiRasterizer extends Rasterizer {
} }
if (isTerminal) { if (isTerminal) {
_composeTerminalScene(); _composeTerminalScene();
}
return toAnsiString(); return toAnsiString();
} }
return _screen;
}
// --- PRIVATE HUD DRAWING HELPERS --- // --- PRIVATE HUD DRAWING HELPERS ---
@@ -787,7 +790,7 @@ class AsciiRasterizer extends Rasterizer {
} }
/// Converts the current frame to a single printable ANSI string /// Converts the current frame to a single printable ANSI string
String toAnsiString() { StringBuffer toAnsiString() {
StringBuffer buffer = StringBuffer(); StringBuffer buffer = StringBuffer();
int? lastForeground; int? lastForeground;
@@ -828,6 +831,6 @@ class AsciiRasterizer extends Rasterizer {
// Reset the terminal color at the very end // Reset the terminal color at the very end
buffer.write('\x1b[0m'); buffer.write('\x1b[0m');
return buffer.toString(); return buffer;
} }
} }

View File

@@ -0,0 +1,36 @@
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
/// Shared terminal orchestration for CLI rasterizers.
abstract class CliRasterizer<T> extends Rasterizer<T> {
/// Resolves the framebuffer dimensions required by this renderer.
///
/// The default uses the full terminal size.
({int width, int height}) terminalFrameBufferSize(int columns, int rows) {
return (width: columns, height: rows);
}
/// Applies terminal-size policy and updates the engine framebuffer.
///
/// Returns `false` when the terminal is too small for this renderer.
bool prepareTerminalFrame(
WolfEngine engine, {
required int columns,
required int rows,
}) {
if (!isTerminalSizeSupported(columns, rows)) {
return false;
}
final size = terminalFrameBufferSize(columns, rows);
engine.setFrameBuffer(size.width, size.height);
return true;
}
/// Builds the standard terminal size warning shown by the CLI host.
String buildTerminalSizeWarning({required int columns, required int rows}) {
return '\x1b[31m[ ERROR ] TERMINAL TOO SMALL\x1b[0m\n\n'
'$terminalSizeRequirement\n'
'Current size: \x1b[33m${columns}x$rows\x1b[0m\n\n'
'Please resize your window to resume the game...';
}
}

View File

@@ -4,7 +4,7 @@ import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_entities.dart'; import 'package:wolf_3d_dart/wolf_3d_entities.dart';
abstract class Rasterizer { abstract class Rasterizer<T> {
late List<double> zBuffer; late List<double> zBuffer;
late int width; late int width;
late int height; late int height;
@@ -40,9 +40,9 @@ abstract class Rasterizer {
/// The main entry point called by the game loop. /// The main entry point called by the game loop.
/// Orchestrates the mathematical rendering pipeline. /// Orchestrates the mathematical rendering pipeline.
dynamic render(WolfEngine engine, FrameBuffer buffer) { T render(WolfEngine engine) {
width = buffer.width; width = engine.frameBuffer.width;
height = buffer.height; height = engine.frameBuffer.height;
// The 3D view typically takes up the top 80% of the screen // The 3D view typically takes up the top 80% of the screen
viewHeight = (height * 0.8).toInt(); viewHeight = (height * 0.8).toInt();
zBuffer = List.filled(projectionWidth, 0.0); zBuffer = List.filled(projectionWidth, 0.0);
@@ -101,7 +101,7 @@ abstract class Rasterizer {
void drawHud(WolfEngine engine); void drawHud(WolfEngine engine);
/// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list). /// Return the finished frame (e.g., the FrameBuffer itself, or an ASCII list).
dynamic finalizeFrame(); T finalizeFrame();
// =========================================================================== // ===========================================================================
// SHARED LIGHTING MATH // SHARED LIGHTING MATH

View File

@@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart';
class SixelRasterizer extends Rasterizer { class SixelRasterizer extends CliRasterizer<String> {
static const double _targetAspectRatio = 4 / 3; static const double _targetAspectRatio = 4 / 3;
static const int _defaultLineHeightPx = 18; static const int _defaultLineHeightPx = 18;
static const double _defaultCellWidthToHeight = 0.55; static const double _defaultCellWidthToHeight = 0.55;
@@ -85,12 +85,18 @@ class SixelRasterizer extends Rasterizer {
} }
@override @override
dynamic render(WolfEngine engine, FrameBuffer buffer) { String render(WolfEngine engine) {
_engine = engine; _engine = engine;
final FrameBuffer scaledBuffer = _createScaledBuffer(buffer); final FrameBuffer originalBuffer = engine.frameBuffer;
final FrameBuffer scaledBuffer = _createScaledBuffer(originalBuffer);
// We only need 8-bit indices for the 256 VGA colors // We only need 8-bit indices for the 256 VGA colors
_screen = Uint8List(scaledBuffer.width * scaledBuffer.height); _screen = Uint8List(scaledBuffer.width * scaledBuffer.height);
return super.render(engine, scaledBuffer); engine.frameBuffer = scaledBuffer;
try {
return super.render(engine);
} finally {
engine.frameBuffer = originalBuffer;
}
} }
@override @override

View File

@@ -3,16 +3,16 @@ import 'dart:math' as math;
import 'package:wolf_3d_dart/wolf_3d_data_types.dart'; import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart';
class SoftwareRasterizer extends Rasterizer { class SoftwareRasterizer extends Rasterizer<FrameBuffer> {
late FrameBuffer _buffer; late FrameBuffer _buffer;
late WolfEngine _engine; late WolfEngine _engine;
// Intercept the base render call to store our references // Intercept the base render call to store our references
@override @override
dynamic render(WolfEngine engine, FrameBuffer buffer) { FrameBuffer render(WolfEngine engine) {
_engine = engine; _engine = engine;
_buffer = buffer; _buffer = engine.frameBuffer;
return super.render(engine, buffer); return super.render(engine);
} }
@override @override

View File

@@ -18,6 +18,7 @@ class WolfEngine {
required this.onGameWon, required this.onGameWon,
required this.audio, required this.audio,
required this.input, required this.input,
required this.frameBuffer,
}) : doorManager = DoorManager( }) : doorManager = DoorManager(
onPlaySound: (sfxId) => audio.playSoundEffect(sfxId), onPlaySound: (sfxId) => audio.playSoundEffect(sfxId),
); );
@@ -48,6 +49,9 @@ class WolfEngine {
/// Polls and processes raw user input into actionable engine commands. /// Polls and processes raw user input into actionable engine commands.
final Wolf3dInput input; final Wolf3dInput input;
/// The shared render target used by all renderer frontends.
FrameBuffer frameBuffer;
/// Handles the detection and movement of secret "Pushwalls". /// Handles the detection and movement of secret "Pushwalls".
final PushwallManager pushwallManager = PushwallManager(); final PushwallManager pushwallManager = PushwallManager();
@@ -86,6 +90,17 @@ class WolfEngine {
isInitialized = true; isInitialized = true;
} }
/// Replaces the shared framebuffer when dimensions change.
void setFrameBuffer(int width, int height) {
if (width <= 0 || height <= 0) {
throw ArgumentError('FrameBuffer dimensions must be greater than zero.');
}
if (frameBuffer.width == width && frameBuffer.height == height) {
return;
}
frameBuffer = FrameBuffer(width, height);
}
/// The primary heartbeat of the engine. /// The primary heartbeat of the engine.
/// ///
/// Updates all world subsystems based on the [elapsed] time. /// Updates all world subsystems based on the [elapsed] time.

View File

@@ -11,6 +11,7 @@ export 'src/engine/managers/pushwall_manager.dart';
export 'src/engine/player/player.dart'; export 'src/engine/player/player.dart';
export 'src/engine/rasterizer/ascii_rasterizer.dart' export 'src/engine/rasterizer/ascii_rasterizer.dart'
show AsciiRasterizer, ColoredChar; show AsciiRasterizer, ColoredChar;
export 'src/engine/rasterizer/cli_rasterizer.dart';
export 'src/engine/rasterizer/rasterizer.dart'; export 'src/engine/rasterizer/rasterizer.dart';
export 'src/engine/rasterizer/sixel_rasterizer.dart'; export 'src/engine/rasterizer/sixel_rasterizer.dart';
export 'src/engine/rasterizer/software_rasterizer.dart'; export 'src/engine/rasterizer/software_rasterizer.dart';

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_renderer/base_renderer.dart'; import 'package:wolf_3d_renderer/base_renderer.dart';
@@ -14,19 +13,28 @@ class WolfAsciiRenderer extends BaseWolfRenderer {
} }
class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> { class _WolfAsciiRendererState extends BaseWolfRendererState<WolfAsciiRenderer> {
static const int _renderWidth = 160;
static const int _renderHeight = 100;
List<List<ColoredChar>> _asciiFrame = []; List<List<ColoredChar>> _asciiFrame = [];
final AsciiRasterizer _asciiRasterizer = AsciiRasterizer(); final AsciiRasterizer _asciiRasterizer = AsciiRasterizer();
@override
void initState() {
super.initState();
if (widget.engine.frameBuffer.width != _renderWidth ||
widget.engine.frameBuffer.height != _renderHeight) {
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
}
}
@override @override
Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64);
@override @override
void performRender() { void performRender() {
setState(() { setState(() {
_asciiFrame = _asciiRasterizer.render( _asciiFrame = _asciiRasterizer.render(widget.engine);
widget.engine,
FrameBuffer(160, 100),
);
}); });
} }

View File

@@ -18,12 +18,22 @@ class WolfFlutterRenderer extends BaseWolfRenderer {
class _WolfFlutterRendererState class _WolfFlutterRendererState
extends BaseWolfRendererState<WolfFlutterRenderer> { extends BaseWolfRendererState<WolfFlutterRenderer> {
final FrameBuffer _frameBuffer = FrameBuffer(320, 200); static const int _renderWidth = 320;
static const int _renderHeight = 200;
final SoftwareRasterizer _rasterizer = SoftwareRasterizer(); final SoftwareRasterizer _rasterizer = SoftwareRasterizer();
ui.Image? _renderedFrame; ui.Image? _renderedFrame;
bool _isRendering = false; bool _isRendering = false;
@override
void initState() {
super.initState();
if (widget.engine.frameBuffer.width != _renderWidth ||
widget.engine.frameBuffer.height != _renderHeight) {
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
}
}
@override @override
Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64);
@@ -32,12 +42,13 @@ class _WolfFlutterRendererState
if (_isRendering) return; if (_isRendering) return;
_isRendering = true; _isRendering = true;
_rasterizer.render(widget.engine, _frameBuffer); final FrameBuffer frameBuffer = widget.engine.frameBuffer;
_rasterizer.render(widget.engine);
ui.decodeImageFromPixels( ui.decodeImageFromPixels(
_frameBuffer.pixels.buffer.asUint8List(), frameBuffer.pixels.buffer.asUint8List(),
_frameBuffer.width, frameBuffer.width,
_frameBuffer.height, frameBuffer.height,
ui.PixelFormat.rgba8888, ui.PixelFormat.rgba8888,
(ui.Image image) { (ui.Image image) {
if (mounted) { if (mounted) {