Files
wolf_dart/packages/wolf_3d_renderer/lib/base_renderer.dart
T

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),
),
),
);
}
}