From 7cb3f25c744b804997e4b13c2ce48309d4665cc2 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 23 Mar 2026 14:18:19 +0100 Subject: [PATCH] feat: Refactor rendering logic to use scheduled presentation and improve performance tracking Signed-off-by: Hans Kokx --- .../wolf_3d_renderer/lib/base_renderer.dart | 98 ++++++++++++++++++- .../lib/wolf_3d_flutter_renderer.dart | 55 ++++++----- .../lib/wolf_3d_glsl_renderer.dart | 49 +++++----- 3 files changed, 153 insertions(+), 49 deletions(-) diff --git a/packages/wolf_3d_renderer/lib/base_renderer.dart b/packages/wolf_3d_renderer/lib/base_renderer.dart index 93a67fb..9b18271 100644 --- a/packages/wolf_3d_renderer/lib/base_renderer.dart +++ b/packages/wolf_3d_renderer/lib/base_renderer.dart @@ -1,10 +1,15 @@ /// Shared Flutter renderer shell for driving the Wolf3D engine from a widget tree. library; +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.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]. abstract class BaseWolfRenderer extends StatefulWidget { /// Engine instance that owns world state and the shared framebuffer. @@ -32,6 +37,17 @@ abstract class BaseWolfRendererState final FocusNode focusNode = FocusNode(); 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 void initState() { @@ -48,12 +64,18 @@ abstract class BaseWolfRendererState return; } - Duration delta = elapsed - _lastTick; + final Duration delta = elapsed - _lastTick; _lastTick = elapsed; + final Stopwatch tickStopwatch = Stopwatch()..start(); widget.engine.tick(delta); + tickStopwatch.stop(); + _tickWindowMicrosTotal += tickStopwatch.elapsedMicroseconds; + _tickWindowCount++; performRender(); + + _maybeLogPerfWindow(); } @override @@ -66,6 +88,80 @@ abstract class BaseWolfRendererState /// Renders the latest engine state into the concrete renderer's output type. 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. Widget buildViewport(BuildContext context); diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index 65c86be..3cc1188 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -29,7 +29,6 @@ class _WolfFlutterRendererState final SoftwareRenderer _renderer = SoftwareRenderer(); ui.Image? _renderedFrame; - bool _isRendering = false; @override void initState() { @@ -45,31 +44,41 @@ class _WolfFlutterRendererState @override Color get scaffoldColor => Colors.black; + @override + void dispose() { + _renderedFrame?.dispose(); + super.dispose(); + } + @override void performRender() { - if (_isRendering) return; - _isRendering = true; + scheduleLatestPresent((onComplete) { + 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; - _renderer.render(widget.engine); - - // Convert the engine-owned framebuffer into a GPU-friendly ui.Image on - // the Flutter side while preserving nearest-neighbor pixel fidelity. - ui.decodeImageFromPixels( - frameBuffer.pixels.buffer.asUint8List(), - frameBuffer.width, - frameBuffer.height, - ui.PixelFormat.rgba8888, - (ui.Image image) { - if (mounted) { - setState(() { - _renderedFrame?.dispose(); - _renderedFrame = image; - }); - } - _isRendering = false; - }, - ); + // Convert the engine-owned framebuffer into a GPU-friendly ui.Image on + // the Flutter side while preserving nearest-neighbor pixel fidelity. + 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(); + } + onComplete(); + }, + ); + }); } @override diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart index 72caf73..46607ce 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_glsl_renderer.dart @@ -43,7 +43,6 @@ class _WolfGlslRendererState extends BaseWolfRendererState { ui.Image? _renderedFrame; ui.FragmentProgram? _shaderProgram; ui.FragmentShader? _shader; - bool _isRendering = false; bool _isShaderUnavailable = false; @override @@ -67,31 +66,31 @@ class _WolfGlslRendererState extends BaseWolfRendererState { @override void performRender() { - if (_isRendering) { - return; - } - _isRendering = true; + scheduleLatestPresent((onComplete) { + 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; - _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; - }, - ); + 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(); + } + onComplete(); + }, + ); + }); } @override