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:
@@ -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<T extends BaseWolfRenderer>
|
||||
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<T extends BaseWolfRenderer>
|
||||
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<T extends BaseWolfRenderer>
|
||||
/// 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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,6 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
||||
ui.Image? _renderedFrame;
|
||||
ui.FragmentProgram? _shaderProgram;
|
||||
ui.FragmentShader? _shader;
|
||||
bool _isRendering = false;
|
||||
bool _isShaderUnavailable = false;
|
||||
|
||||
@override
|
||||
@@ -67,31 +66,31 @@ class _WolfGlslRendererState extends BaseWolfRendererState<WolfGlslRenderer> {
|
||||
|
||||
@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
|
||||
|
||||
Reference in New Issue
Block a user