feat: Add GLSL renderer and implement FPS overlay across rendering backends

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-19 19:12:39 +01:00
parent c62ea013ba
commit c8cd2cb144
9 changed files with 344 additions and 8 deletions

View File

@@ -0,0 +1,182 @@
/// Flutter widget that applies a GLSL post-process over Wolf3D frames.
library;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_renderer.dart';
import 'package:wolf_3d_renderer/base_renderer.dart';
import 'package:wolf_3d_renderer/wolf_3d_asset_painter.dart';
/// Displays software-rendered frames through a GLSL post-processing pass.
class WolfGlslRenderer extends BaseWolfRenderer {
/// Callback when shader loading fails and software fallback should be used.
final VoidCallback? onUnavailable;
/// Creates a GLSL renderer bound to [engine].
const WolfGlslRenderer({
required super.engine,
this.onUnavailable,
super.key,
});
@override
State<WolfGlslRenderer> createState() => _WolfGlslRendererState();
}
class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
static const int _renderWidth = 320;
static const int _renderHeight = 200;
final SoftwareRenderer _renderer = SoftwareRenderer();
ui.Image? _renderedFrame;
ui.FragmentProgram? _shaderProgram;
ui.FragmentShader? _shader;
bool _isRendering = false;
bool _isShaderUnavailable = false;
@override
void initState() {
super.initState();
if (widget.engine.frameBuffer.width != _renderWidth ||
widget.engine.frameBuffer.height != _renderHeight) {
widget.engine.setFrameBuffer(_renderWidth, _renderHeight);
}
_loadShader();
}
@override
Color get scaffoldColor => widget.engine.difficulty == null
? _colorFromRgb(widget.engine.menuBackgroundRgb)
: const Color.fromARGB(255, 4, 64, 64);
@override
void dispose() {
_renderedFrame?.dispose();
super.dispose();
}
@override
void performRender() {
if (_isRendering) {
return;
}
_isRendering = true;
final FrameBuffer frameBuffer = widget.engine.frameBuffer;
_renderer.render(widget.engine);
ui.decodeImageFromPixels(
frameBuffer.pixels.buffer.asUint8List(),
frameBuffer.width,
frameBuffer.height,
ui.PixelFormat.rgba8888,
(ui.Image image) {
if (mounted) {
setState(() {
_renderedFrame?.dispose();
_renderedFrame = image;
});
} else {
image.dispose();
}
_isRendering = false;
},
);
}
@override
Widget buildViewport(BuildContext context) {
if (_renderedFrame == null) {
return const CircularProgressIndicator(color: Colors.white24);
}
if (_isShaderUnavailable || _shader == null) {
// Keep frames visible even if GLSL initialization failed.
return Padding(
padding: const EdgeInsets.all(16.0),
child: AspectRatio(
aspectRatio: 4 / 3,
child: WolfAssetPainter.frame(_renderedFrame),
),
);
}
return Padding(
padding: const EdgeInsets.all(16.0),
child: AspectRatio(
aspectRatio: 4 / 3,
child: CustomPaint(
painter: _GlslFramePainter(
frame: _renderedFrame!,
shader: _shader!,
),
child: const SizedBox.expand(),
),
),
);
}
Future<void> _loadShader() async {
try {
final ui.FragmentProgram program = await ui.FragmentProgram.fromAsset(
'packages/wolf_3d_renderer/shaders/wolf_world.frag',
);
if (!mounted) {
return;
}
setState(() {
_shaderProgram = program;
_shader = _shaderProgram!.fragmentShader();
_isShaderUnavailable = false;
});
} catch (_) {
if (!mounted) {
return;
}
setState(() {
_isShaderUnavailable = true;
});
widget.onUnavailable?.call();
}
}
Color _colorFromRgb(int rgb) {
return Color(0xFF000000 | (rgb & 0x00FFFFFF));
}
}
class _GlslFramePainter extends CustomPainter {
final ui.Image frame;
final ui.FragmentShader shader;
_GlslFramePainter({
required this.frame,
required this.shader,
});
@override
void paint(Canvas canvas, Size size) {
final double texelX = frame.width > 0 ? 1.0 / frame.width : 1.0;
final double texelY = frame.height > 0 ? 1.0 / frame.height : 1.0;
shader
..setFloat(0, size.width)
..setFloat(1, size.height)
..setFloat(2, texelX)
..setFloat(3, texelY)
..setImageSampler(0, frame);
final Paint paint = Paint()
..shader = shader
..filterQuality = FilterQuality.none;
canvas.drawRect(Offset.zero & size, paint);
}
@override
bool shouldRepaint(covariant _GlslFramePainter oldDelegate) {
return oldDelegate.frame != frame || oldDelegate.shader != shader;
}
}