7cb3f25c74
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
188 lines
5.1 KiB
Dart
188 lines
5.1 KiB
Dart
/// 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.
|
|
final WolfEngine engine;
|
|
|
|
/// Optional key handler invoked by the focused renderer shell.
|
|
final void Function(KeyEvent event)? onKeyEvent;
|
|
|
|
/// Creates a renderer bound to [engine].
|
|
const BaseWolfRenderer({
|
|
required this.engine,
|
|
this.onKeyEvent,
|
|
super.key,
|
|
});
|
|
}
|
|
|
|
/// Base [State] implementation that provides a ticker-driven render loop.
|
|
abstract class BaseWolfRendererState<T extends BaseWolfRenderer>
|
|
extends State<T>
|
|
with SingleTickerProviderStateMixin {
|
|
/// Per-frame ticker used to advance the engine and request renders.
|
|
late final Ticker gameLoop;
|
|
|
|
/// Focus node used by the enclosing [KeyboardListener].
|
|
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() {
|
|
super.initState();
|
|
gameLoop = createTicker(_tick)..start();
|
|
focusNode.requestFocus();
|
|
}
|
|
|
|
void _tick(Duration elapsed) {
|
|
if (!widget.engine.isInitialized) return;
|
|
|
|
if (_lastTick == Duration.zero) {
|
|
_lastTick = elapsed;
|
|
return;
|
|
}
|
|
|
|
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
|
|
void dispose() {
|
|
gameLoop.dispose();
|
|
focusNode.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// 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);
|
|
|
|
/// Background color used by the surrounding scaffold.
|
|
Color get scaffoldColor;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: scaffoldColor,
|
|
body: KeyboardListener(
|
|
focusNode: focusNode,
|
|
autofocus: true,
|
|
onKeyEvent: (event) {
|
|
widget.onKeyEvent?.call(event);
|
|
},
|
|
child: Center(
|
|
child: buildViewport(context),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|