[flutter] Reduce allocations by using a block/bump allocator for rendering commands.

This commit is contained in:
Mario Zechner 2022-11-23 10:19:41 +01:00
parent 40df75abf4
commit 28312004e3
9 changed files with 170 additions and 41 deletions

View File

@ -6,13 +6,14 @@ import 'package:flutter/material.dart';
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
class SpineComponent extends PositionComponent {
static final Finalizer<SpineComponent> _finalizer = Finalizer((spineComponent) => spineComponent.dispose());
final BoundsProvider _boundsProvider;
final SkeletonDrawable _drawable;
late final Bounds _bounds;
final bool _ownsDrawable;
SpineComponent(this._drawable, {
bool ownsDrawable = false,
bool ownsDrawable = true,
BoundsProvider boundsProvider = const SetupPoseBounds(),
super.position,
super.scale,
@ -26,6 +27,7 @@ class SpineComponent extends PositionComponent {
_drawable.update(0);
_bounds = _boundsProvider.computeBounds(_drawable);
size = Vector2(_bounds.width, _bounds.height);
_finalizer.attach(this, this);
}
static Future<SpineComponent> fromAssets(String atlasFile, String skeletonFile, {
@ -50,9 +52,11 @@ class SpineComponent extends PositionComponent {
priority: priority);
}
@override
void onRemove() {
if (_ownsDrawable) _drawable.dispose();
void dispose() {
if (_ownsDrawable) {
_drawable.dispose();
}
_finalizer.detach(this);
}
@override
@ -92,12 +96,18 @@ class SimpleFlameExample extends FlameGame {
spineboy.animationState.setAnimationByName(0, "walk", true);
await add(spineboy);
}
@override
void onDetach() {
// Dispose the native resources that have been loaded for spineboy.
spineboy.dispose();
}
}
class PreloadAndShareSpineDataExample extends FlameGame {
late final SkeletonData cachedSkeletonData;
late final Atlas cachedAtlas;
late final List<SpineComponent> spineboys;
late final List<SpineComponent> spineboys = [];
@override
Future<void> onLoad() async {
@ -109,7 +119,7 @@ class PreloadAndShareSpineDataExample extends FlameGame {
// gets their own SkeletonDrawable copy derived from the cached data. The
// SkeletonDrawable copies do not own the underlying skeleton data and atlas.
final rng = Random();
for (int i = 0; i < 100; i++) {
for (int i = 0; i < 50; i++) {
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
final scale = 0.1 + rng.nextDouble() * 0.2;
final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y);
@ -119,15 +129,19 @@ class PreloadAndShareSpineDataExample extends FlameGame {
position: position
);
spineboy.animationState.setAnimationByName(0, "walk", true);
spineboys.add(spineboy);
await add(spineboy);
}
}
@override
void onRemove() {
void onDetach() {
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed
cachedAtlas.dispose();
cachedSkeletonData.dispose();
for (final spineboy in spineboys) {
spineboy.dispose();
}
}
}

View File

@ -126,7 +126,7 @@ class ExampleSelector extends StatelessWidget {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initSpineFlutter();
await initSpineFlutter(enableMemoryDebugging: false);
runApp(const MaterialApp(
title: "Spine Examples",
home: ExampleSelector()

View File

@ -18,10 +18,11 @@ import 'package:flutter/foundation.dart' show kIsWeb;
late SpineFlutterBindings _bindings;
late Allocator _allocator;
Future<void> initSpineFlutter() async {
Future<void> initSpineFlutter({bool enableMemoryDebugging = false}) async {
final ffi = await initSpineFlutterFFI();
_bindings = SpineFlutterBindings(ffi.dylib);
_allocator = ffi.allocator;
if (enableMemoryDebugging) _bindings.spine_enable_debug_extension(-1);
return;
}

View File

@ -44,6 +44,20 @@ class SpineFlutterBindings {
late final _spine_minor_version =
_spine_minor_versionPtr.asFunction<int Function()>();
void spine_enable_debug_extension(
int enable,
) {
return _spine_enable_debug_extension(
enable,
);
}
late final _spine_enable_debug_extensionPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int)>>(
'spine_enable_debug_extension');
late final _spine_enable_debug_extension =
_spine_enable_debug_extensionPtr.asFunction<void Function(int)>();
void spine_report_leaks() {
return _spine_report_leaks();
}

View File

@ -224,10 +224,8 @@ class _SpineWidgetState extends State<SpineWidget> {
@override
Widget build(BuildContext context) {
if (widget._controller._drawable != null) {
print("Skeleton loaded, rebuilding painter");
return _SpineRenderObjectWidget(widget._controller._drawable!, widget._controller, widget._fit, widget._alignment, widget._boundsProvider, widget._sizedByBounds);
} else {
print("Skeleton not loaded yet");
return const SizedBox();
}
}

View File

@ -19,3 +19,10 @@ set_target_properties(esotericsoftware_spine_flutter PROPERTIES
target_include_directories(esotericsoftware_spine_flutter PUBLIC spine-cpp/include)
target_compile_definitions(esotericsoftware_spine_flutter PUBLIC DART_SHARED_LIB)
if(${SPINE_FLUTTER_TESTBED})
set (CMAKE_CXX_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=undefined")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=undefined")
add_executable(spine_flutter_test main.cpp spine_flutter.cpp ${SPINE_SOURCES})
include_directories(spine_flutter_test PUBLIC spine-cpp/include)
endif()

View File

@ -0,0 +1,22 @@
#include "spine/spine.h"
#include "spine_flutter.h"
using namespace spine;
int main(int argc, char** argv) {
int atlasLength = 0;
void* atlasData = SpineExtension::getInstance()->_readFile("/Users/badlogic/workspaces/spine-runtimes/spine-flutter/example/assets/spineboy.atlas", &atlasLength);
uint8_t* cstringAtlas = SpineExtension::calloc<uint8_t>(atlasLength + 1, __FILE__, __LINE__);
memcpy(cstringAtlas, atlasData, atlasLength);
int dataLength = 0;
uint8_t* data = (uint8_t*)SpineExtension::getInstance()->_readFile("/Users/badlogic/workspaces/spine-runtimes/spine-flutter/example/assets/spineboy-pro.skel", &dataLength);
spine_atlas atlas = spine_atlas_load((const utf8*)cstringAtlas);
spine_skeleton_data_result result = spine_skeleton_data_load_binary(atlas, data, dataLength);
spine_skeleton_drawable drawable = spine_skeleton_drawable_create(spine_skeleton_data_result_get_data(result));
spine_render_command cmd = spine_skeleton_drawable_render(drawable);
while (cmd) {
uint16_t *indices = spine_render_command_get_indices(cmd);
cmd = spine_render_command_get_next(cmd);
}
}

View File

@ -34,6 +34,75 @@
using namespace spine;
struct Block {
int size;
int allocated;
uint8_t* memory;
int free() {
return size - allocated;
}
bool canFit(int numBytes) {
return free() >= numBytes;
}
uint8_t* allocate(int numBytes) {
uint8_t *ptr = memory + allocated;
memset(ptr, 0, numBytes);
allocated += numBytes;
return ptr;
}
};
class BlockAllocator : public SpineObject{
int initialBlockSize;
Vector<Block> blocks;
public:
BlockAllocator(int initialBlockSize) : initialBlockSize(initialBlockSize) {
blocks.add(newBlock(initialBlockSize));
}
~BlockAllocator() {
for (int i = 0; i < blocks.size(); i++) {
SpineExtension::free(blocks[i].memory, __FILE__, __LINE__);
}
}
Block newBlock(int numBytes) {
Block block = {MathUtil::max(initialBlockSize, numBytes), 0, nullptr};
block.memory = SpineExtension::alloc<uint8_t>(block.size, __FILE__, __LINE__);
return block;
}
template<typename T>
T *allocate(size_t num) {
return (T *) _allocate((int)(sizeof(T) * num));
}
void compress() {
int totalSize = 0;
for (int i = 0; i < blocks.size(); i++) {
totalSize += blocks[i].size;
SpineExtension::free(blocks[i].memory, __FILE__, __LINE__);
}
blocks.clear();
blocks.add(newBlock(totalSize));
}
private:
void *_allocate(int numBytes) {
// 16-byte align allocations
int alignedNumBytes = numBytes + (numBytes % 16 != 0 ? 16 - (numBytes % 16) : 0);
Block *block = &blocks[blocks.size() - 1];
if (!block->canFit(alignedNumBytes)) {
blocks.add(newBlock(MathUtil::max(initialBlockSize, alignedNumBytes)));
block = &blocks[blocks.size() - 1];
}
return block->allocate(alignedNumBytes);
}
};
struct AnimationStateEvent {
EventType type;
TrackEntry *entry;
@ -88,6 +157,7 @@ typedef struct _spine_skeleton_drawable {
spine_animation_state_events animationStateEvents;
void *clipping;
_spine_render_command *renderCommand;
BlockAllocator *allocator;
} _spine_skeleton_drawable;
typedef struct _spine_skin_entry {
@ -103,9 +173,24 @@ typedef struct _spine_skin_entries {
static Color NULL_COLOR(0, 0, 0, 0);
static SpineExtension *defaultExtension = nullptr;
static DebugExtension *debugExtension = nullptr;
static void initExtensions() {
if (defaultExtension == nullptr) {
defaultExtension = new DefaultSpineExtension();
debugExtension = new DebugExtension(defaultExtension);
}
}
spine::SpineExtension *spine::getDefaultExtension() {
// return new spine::DebugExtension(new spine::DefaultSpineExtension());
return new spine::DefaultSpineExtension();
initExtensions();
return defaultExtension;
}
void spine_enable_debug_extension(int enable) {
initExtensions();
SpineExtension::setInstance(enable ? debugExtension : defaultExtension);
}
int32_t spine_major_version() {
@ -117,7 +202,9 @@ int32_t spine_minor_version() {
}
void spine_report_leaks() {
// ((DebugExtension*)spine::SpineExtension::getInstance())->reportLeaks();
initExtensions();
debugExtension->reportLeaks();
fflush(stdout);
}
// Color
@ -515,13 +602,13 @@ void spine_skeleton_data_dispose(spine_skeleton_data data) {
}
// RenderCommand
_spine_render_command *spine_render_command_create(int numVertices, int32_t numIndices, spine_blend_mode blendMode, int32_t pageIndex) {
_spine_render_command *cmd = SpineExtension::alloc<_spine_render_command>(1, __FILE__, __LINE__);
cmd->positions = SpineExtension::alloc<float>(numVertices << 1, __FILE__, __LINE__);
cmd->uvs = SpineExtension::alloc<float>(numVertices << 1, __FILE__, __LINE__);
cmd->colors = SpineExtension::alloc<int32_t>(numVertices, __FILE__, __LINE__);
static _spine_render_command *spine_render_command_create(BlockAllocator &allocator, int numVertices, int32_t numIndices, spine_blend_mode blendMode, int32_t pageIndex) {
_spine_render_command *cmd = allocator.allocate<_spine_render_command>(1);
cmd->positions = allocator.allocate<float>(numVertices << 1);
cmd->uvs = allocator.allocate<float>(numVertices << 1);
cmd->colors = allocator.allocate<int32_t>(numVertices);
cmd->numVertices = numVertices;
cmd->indices = SpineExtension::alloc<uint16_t>(numIndices, __FILE__, __LINE__);
cmd->indices = allocator.allocate<uint16_t>(numIndices);
cmd->numIndices = numIndices;
cmd->blendMode = blendMode;
cmd->atlasPage = pageIndex;
@ -529,15 +616,6 @@ _spine_render_command *spine_render_command_create(int numVertices, int32_t numI
return cmd;
}
void spine_render_command_dispose(_spine_render_command *cmd) {
if (!cmd) return;
if (cmd->positions) SpineExtension::free(cmd->positions, __FILE__, __LINE__);
if (cmd->uvs) SpineExtension::free(cmd->uvs, __FILE__, __LINE__);
if (cmd->colors) SpineExtension::free(cmd->colors, __FILE__, __LINE__);
if (cmd->indices) SpineExtension::free(cmd->indices, __FILE__, __LINE__);
SpineExtension::free(cmd, __FILE__, __LINE__);
}
// SkeletonDrawable
spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData) {
@ -552,6 +630,7 @@ spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skele
drawable->animationStateEvents = (spine_animation_state_events) listener;
state->setListener(listener);
drawable->clipping = new (__FILE__, __LINE__) SkeletonClipping();
drawable->allocator = new (__FILE__, __LINE__) BlockAllocator(2048);
return (spine_skeleton_drawable) drawable;
}
@ -563,11 +642,7 @@ void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable) {
if (_drawable->animationStateData) delete (AnimationStateData *) _drawable->animationStateData;
if (_drawable->animationStateEvents) delete (Vector<AnimationStateEvent> *) (_drawable->animationStateEvents);
if (_drawable->clipping) delete (SkeletonClipping *) _drawable->clipping;
while (_drawable->renderCommand) {
_spine_render_command *cmd = _drawable->renderCommand;
_drawable->renderCommand = cmd->next;
spine_render_command_dispose(cmd);
}
if (_drawable->allocator) delete (BlockAllocator *) _drawable->allocator;
SpineExtension::free(drawable, __FILE__, __LINE__);
}
@ -576,11 +651,8 @@ spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable draw
if (!_drawable) return nullptr;
if (!_drawable->skeleton) return nullptr;
while (_drawable->renderCommand) {
_spine_render_command *cmd = _drawable->renderCommand;
_drawable->renderCommand = cmd->next;
spine_render_command_dispose(cmd);
}
_drawable->allocator->compress();
_drawable->renderCommand = nullptr;
Vector<unsigned short> quadIndices;
quadIndices.add(0);
@ -671,7 +743,7 @@ spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable draw
indicesCount = (int32_t) (clipper.getClippedTriangles().size());
}
_spine_render_command *cmd = spine_render_command_create(verticesCount, indicesCount, (spine_blend_mode) slot.getData().getBlendMode(), pageIndex);
_spine_render_command *cmd = spine_render_command_create(*_drawable->allocator, verticesCount, indicesCount, (spine_blend_mode) slot.getData().getBlendMode(), pageIndex);
memcpy(cmd->positions, vertices->buffer(), (verticesCount << 1) * sizeof(float));
memcpy(cmd->uvs, uvs->buffer(), (verticesCount << 1) * sizeof(float));
@ -1345,7 +1417,7 @@ spine_path_constraint spine_skeleton_find_path_constraint(spine_skeleton skeleto
}
spine_bounds spine_skeleton_get_bounds(spine_skeleton skeleton) {
_spine_bounds *bounds = SpineExtension::calloc<_spine_bounds>(1, __FILE__, __LINE__);
_spine_bounds *bounds = (_spine_bounds*)malloc(sizeof(_spine_bounds));
if (skeleton == nullptr) return (spine_bounds) bounds;
Skeleton *_skeleton = (Skeleton *) skeleton;
Vector<float> vertices;

View File

@ -169,6 +169,7 @@ typedef enum spine_rotate_mode {
SPINE_FLUTTER_EXPORT int32_t spine_major_version();
SPINE_FLUTTER_EXPORT int32_t spine_minor_version();
SPINE_FLUTTER_EXPORT void spine_enable_debug_extension(int enable);
SPINE_FLUTTER_EXPORT void spine_report_leaks();
SPINE_FLUTTER_EXPORT float spine_color_get_r(spine_color color);