add fps counters and headroom

This commit is contained in:
Nick Fisher
2025-03-01 13:02:05 +08:00
parent 1b1de0b7c0
commit ffc256228a
2 changed files with 119 additions and 31 deletions

View File

@@ -26,12 +26,19 @@ class ThermionTextureWidget extends StatefulWidget {
///
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
const ThermionTextureWidget(
{super.key,
required this.viewer,
required this.view,
this.initial,
this.onResize});
///
/// When true, an FPS counter will be displayed at the top right of the widget
///
final bool showFpsCounter;
const ThermionTextureWidget({
super.key,
required this.viewer,
required this.view,
this.initial,
this.onResize,
this.showFpsCounter = false,
});
@override
State<StatefulWidget> createState() {
@@ -40,21 +47,27 @@ class ThermionTextureWidget extends StatefulWidget {
}
class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
PlatformTextureDescriptor? _texture;
static final _views = <t.View>[];
final _logger = Logger("_ThermionTextureWidgetState");
int _fps = 0;
int _frameCount = 0;
int _frameRequestCount = 0;
int _frameRequestPercentage = 0;
int _lastFpsUpdateTime = 0;
Timer? _fpsUpdateTimer;
@override
void dispose() {
super.dispose();
_views.remove(widget.view);
if(_texture != null) {
if (_texture != null) {
ThermionFlutterPlatform.instance.destroyTextureDescriptor(_texture!);
}
_fpsUpdateTimer?.cancel();
_states.remove(this);
}
@@ -64,6 +77,23 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
throw Exception("View already embedded in a widget");
}
_views.add(widget.view);
// Start FPS counter update timer if enabled
if (widget.showFpsCounter) {
_fpsUpdateTimer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() {
_fps = _frameCount;
_frameRequestPercentage = _frameCount > 0
? (_frameCount / _frameRequestCount * 100).round()
: 0;
_frameCount = 0;
_frameRequestCount = 0;
});
}
});
}
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
await widget.viewer.initialized;
@@ -71,9 +101,9 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
var size = ((context.findRenderObject()) as RenderBox).size;
_logger.info(
"Widget size in logical pixels ${size} (pixel ratio : $dpr)");
_logger
.info("Widget size in logical pixels ${size} (pixel ratio : $dpr)");
var width = (size.width * dpr).ceil();
var height = (size.height * dpr).ceil();
@@ -111,7 +141,7 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
if (mounted) {
setState(() {});
}
if(texture != null) {
if (texture != null) {
ThermionFlutterPlatform.instance.destroyTextureDescriptor(texture);
}
@@ -126,6 +156,7 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
static final _states = <_ThermionTextureWidgetState>{};
int lastRender = 0;
int _headroomInMs = 5;
///
/// Each instance of ThermionTextureWidget in the widget hierarchy must
@@ -144,18 +175,32 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
if (!mounted) {
return;
}
if (widget.showFpsCounter) {
_frameRequestCount++;
}
WidgetsBinding.instance.scheduleFrameCallback((d) async {
if (!mounted) {
return;
}
if (widget.viewer.rendering && !_rendering && _resizing.isEmpty && (d.inMilliseconds - lastRender > widget.viewer.msPerFrame)) {
if (widget.viewer.rendering &&
!_rendering &&
_resizing.isEmpty &&
(d.inMilliseconds - lastRender >
widget.viewer.msPerFrame - _headroomInMs)) {
_rendering = true;
if (this == _states.first && _texture != null) {
await widget.viewer.requestFrame();
lastRender = d.inMilliseconds;
if (widget.showFpsCounter) {
_frameCount++;
}
}
if(_texture != null) {
await ThermionFlutterPlatform.instance.markTextureFrameAvailable(_texture!);
if (_texture != null) {
await ThermionFlutterPlatform.instance
.markTextureFrameAvailable(_texture!);
}
_rendering = false;
}
@@ -197,7 +242,8 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
_logger.info(
"Resizing texture to dimensions ${newWidth}x${newHeight} (pixel ratio : $dpr)");
_texture = await ThermionFlutterPlatform.instance.resizeTexture(_texture!, widget.view, newWidth, newHeight);
_texture = await ThermionFlutterPlatform.instance
.resizeTexture(_texture!, widget.view, newWidth, newHeight);
_logger.info(
"Resized texture to dimensions ${_texture!.width}x${_texture!.height} (pixel ratio : $dpr)");
@@ -225,15 +271,50 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
}
return ResizeObserver(
onResized: _resize,
child: Stack(children: [
onResized: _resize,
child: Stack(
children: [
Positioned.fill(
child: Texture(
key: ObjectKey("flutter_texture_${_texture!.flutterTextureId}"),
textureId: _texture!.flutterTextureId,
filterQuality: FilterQuality.none,
freeze: false,
))
]));
)),
if (widget.showFpsCounter)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(4),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'$_fps FPS',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
Text(
'Render: $_frameRequestPercentage%',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -13,7 +13,10 @@ Future kDefaultResizeCallback(Size size, t.View view, double pixelRatio) async {
var far = await camera.getCullingFar();
var focalLength = await camera.getFocalLength();
await camera.setLensProjection(near:near, far:far, focalLength: focalLength,
await camera.setLensProjection(
near: near,
far: far,
focalLength: focalLength,
aspect: size.width.toDouble() / size.height.toDouble());
}
@@ -29,21 +32,23 @@ class ThermionWidget extends StatefulWidget {
final t.View? view;
///
/// A callback to invoke whenever this widget and the underlying surface are
/// resized. If a callback is not explicitly provided, the default callback
/// will be run, which changes the aspect ratio for the active camera in
/// the View managed by this widget. If you specify your own callback,
/// A callback to invoke whenever this widget and the underlying surface are
/// resized. If a callback is not explicitly provided, the default callback
/// will be run, which changes the aspect ratio for the active camera in
/// the View managed by this widget. If you specify your own callback,
/// you probably want to preserve this behaviour (otherwise the aspect ratio)
/// will be incorrect.
///
/// will be incorrect.
///
/// To completely disable the resize callback, pass [null].
///
/// IMPORTANT - size is specified in physical pixels, not logical pixels.
/// If you need to work with Flutter dimensions, divide [size] by
///
/// IMPORTANT - size is specified in physical pixels, not logical pixels.
/// If you need to work with Flutter dimensions, divide [size] by
/// [pixelRatio].
///
final Future Function(Size size, t.View view, double pixelRatio)? onResize;
final bool showFpsCounter;
///
/// The content to render before the texture widget is available.
/// The default is a solid red Container, intentionally chosen to make it clear that there will be at least one frame where the Texture widget is not being rendered.
@@ -55,6 +60,7 @@ class ThermionWidget extends StatefulWidget {
this.initial,
required this.viewer,
this.view,
this.showFpsCounter = false,
this.onResize = kDefaultResizeCallback})
: super(key: key);
@@ -98,6 +104,7 @@ class _ThermionWidgetState extends State<ThermionWidget> {
initial: widget.initial,
viewer: widget.viewer,
view: view!,
showFpsCounter:widget.showFpsCounter,
onResize: widget.onResize);
}
}