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