feat: Refactor rendering logic to use scheduled presentation and improve performance tracking

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-23 14:18:19 +01:00
parent a66ccf52c5
commit 7cb3f25c74
3 changed files with 153 additions and 49 deletions
@@ -1,10 +1,15 @@
/// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree. /// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree.
library; library;
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart'; import 'package:wolf_3d_dart/wolf_3d_engine.dart';
typedef PresentFrameAction = void Function(VoidCallback onComplete);
/// Base widget for renderers that present frames from a [WolfEngine]. /// Base widget for renderers that present frames from a [WolfEngine].
abstract class BaseWolfRenderer extends StatefulWidget { abstract class BaseWolfRenderer extends StatefulWidget {
/// Engine instance that owns world state and the shared framebuffer. /// Engine instance that owns world state and the shared framebuffer.
@@ -32,6 +37,17 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
Duration _lastTick = Duration.zero; Duration _lastTick = Duration.zero;
bool _isPresenting = false;
PresentFrameAction? _queuedPresentAction;
int _tickWindowCount = 0;
int _presentWindowCount = 0;
int _coalescedPresentWindowCount = 0;
int _tickWindowMicrosTotal = 0;
int _renderWindowMicrosTotal = 0;
int _presentWindowMicrosTotal = 0;
static const int _perfLogWindowFrames = 180;
@override @override
void initState() { void initState() {
@@ -48,12 +64,18 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
return; return;
} }
Duration delta = elapsed - _lastTick; final Duration delta = elapsed - _lastTick;
_lastTick = elapsed; _lastTick = elapsed;
final Stopwatch tickStopwatch = Stopwatch()..start();
widget.engine.tick(delta); widget.engine.tick(delta);
tickStopwatch.stop();
_tickWindowMicrosTotal += tickStopwatch.elapsedMicroseconds;
_tickWindowCount++;
performRender(); performRender();
_maybeLogPerfWindow();
} }
@override @override
@@ -66,6 +88,80 @@ abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
/// Renders the latest engine state into the concrete renderer's output type. /// Renders the latest engine state into the concrete renderer's output type.
void performRender(); void performRender();
/// Schedules presentation work while coalescing to the latest requested frame.
@protected
void scheduleLatestPresent(PresentFrameAction action) {
if (_isPresenting) {
_queuedPresentAction = action;
_coalescedPresentWindowCount++;
return;
}
_runPresentAction(action);
}
/// Records CPU-side rendering stage duration in microseconds.
@protected
void recordRenderStageMicros(int microseconds) {
_renderWindowMicrosTotal += microseconds;
}
void _runPresentAction(PresentFrameAction action) {
_isPresenting = true;
final Stopwatch presentStopwatch = Stopwatch()..start();
bool isCompleted = false;
void onComplete() {
if (isCompleted) {
return;
}
isCompleted = true;
presentStopwatch.stop();
_presentWindowMicrosTotal += presentStopwatch.elapsedMicroseconds;
_presentWindowCount++;
_isPresenting = false;
if (!mounted) {
return;
}
final PresentFrameAction? nextAction = _queuedPresentAction;
_queuedPresentAction = null;
if (nextAction != null) {
scheduleMicrotask(() => _runPresentAction(nextAction));
}
}
action(onComplete);
}
void _maybeLogPerfWindow() {
if (!kDebugMode || _tickWindowCount < _perfLogWindowFrames) {
return;
}
final double avgTickMs = _tickWindowMicrosTotal / _tickWindowCount / 1000.0;
final double avgRenderMs =
_renderWindowMicrosTotal / _tickWindowCount / 1000.0;
final double avgPresentMs = _presentWindowCount > 0
? _presentWindowMicrosTotal / _presentWindowCount / 1000.0
: 0.0;
debugPrint(
'Renderer perf window ($_tickWindowCount frames): '
'avg tick ${avgTickMs.toStringAsFixed(2)}ms, '
'avg render ${avgRenderMs.toStringAsFixed(2)}ms, '
'avg present ${avgPresentMs.toStringAsFixed(2)}ms, '
'coalesced $_coalescedPresentWindowCount',
);
_tickWindowCount = 0;
_presentWindowCount = 0;
_coalescedPresentWindowCount = 0;
_tickWindowMicrosTotal = 0;
_renderWindowMicrosTotal = 0;
_presentWindowMicrosTotal = 0;
}
/// Builds the visible viewport widget for the latest rendered frame. /// Builds the visible viewport widget for the latest rendered frame.
Widget buildViewport(BuildContext context); Widget buildViewport(BuildContext context);
@@ -29,7 +29,6 @@ class _WolfFlutterRendererState
final SoftwareRenderer _renderer = SoftwareRenderer(); final SoftwareRenderer _renderer = SoftwareRenderer();
ui.Image? _renderedFrame; ui.Image? _renderedFrame;
bool _isRendering = false;
@override @override
void initState() { void initState() {
@@ -45,31 +44,41 @@ class _WolfFlutterRendererState
@override @override
Color get scaffoldColor => Colors.black; Color get scaffoldColor => Colors.black;
@override
void dispose() {
_renderedFrame?.dispose();
super.dispose();
}
@override @override
void performRender() { void performRender() {
if (_isRendering) return; scheduleLatestPresent((onComplete) {
_isRendering = true; final FrameBuffer frameBuffer = widget.engine.frameBuffer;
final Stopwatch renderStopwatch = Stopwatch()..start();
_renderer.render(widget.engine);
renderStopwatch.stop();
recordRenderStageMicros(renderStopwatch.elapsedMicroseconds);
final FrameBuffer frameBuffer = widget.engine.frameBuffer; // Convert the engine-owned framebuffer into a GPU-friendly ui.Image on
_renderer.render(widget.engine); // the Flutter side while preserving nearest-neighbor pixel fidelity.
ui.decodeImageFromPixels(
// Convert the engine-owned framebuffer into a GPU-friendly ui.Image on frameBuffer.pixels.buffer.asUint8List(),
// the Flutter side while preserving nearest-neighbor pixel fidelity. frameBuffer.width,
ui.decodeImageFromPixels( frameBuffer.height,
frameBuffer.pixels.buffer.asUint8List(), ui.PixelFormat.rgba8888,
frameBuffer.width, (ui.Image image) {
frameBuffer.height, if (mounted) {
ui.PixelFormat.rgba8888, setState(() {
(ui.Image image) { _renderedFrame?.dispose();
if (mounted) { _renderedFrame = image;
setState(() { });
_renderedFrame?.dispose(); } else {
_renderedFrame = image; image.dispose();
}); }
} onComplete();
_isRendering = false; },
}, );
); });
} }
@override @override
@@ -43,7 +43,6 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
ui.Image? _renderedFrame; ui.Image? _renderedFrame;
ui.FragmentProgram? _shaderProgram; ui.FragmentProgram? _shaderProgram;
ui.FragmentShader? _shader; ui.FragmentShader? _shader;
bool _isRendering = false;
bool _isShaderUnavailable = false; bool _isShaderUnavailable = false;
@override @override
@@ -67,31 +66,31 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
@override @override
void performRender() { void performRender() {
if (_isRendering) { scheduleLatestPresent((onComplete) {
return; final FrameBuffer frameBuffer = widget.engine.frameBuffer;
} final Stopwatch renderStopwatch = Stopwatch()..start();
_isRendering = true; _renderer.render(widget.engine);
renderStopwatch.stop();
recordRenderStageMicros(renderStopwatch.elapsedMicroseconds);
final FrameBuffer frameBuffer = widget.engine.frameBuffer; ui.decodeImageFromPixels(
_renderer.render(widget.engine); frameBuffer.pixels.buffer.asUint8List(),
frameBuffer.width,
ui.decodeImageFromPixels( frameBuffer.height,
frameBuffer.pixels.buffer.asUint8List(), ui.PixelFormat.rgba8888,
frameBuffer.width, (ui.Image image) {
frameBuffer.height, if (mounted) {
ui.PixelFormat.rgba8888, setState(() {
(ui.Image image) { _renderedFrame?.dispose();
if (mounted) { _renderedFrame = image;
setState(() { });
_renderedFrame?.dispose(); } else {
_renderedFrame = image; image.dispose();
}); }
} else { onComplete();
image.dispose(); },
} );
_isRendering = false; });
},
);
} }
@override @override