๐บ๐ธ English | ๐จ๐ณ ็ฎไฝไธญๆ
๐ฅ FlameBarrage Engine
A High-Performance, Hardware-Accelerated Barrage Rendering Engine for Flutter
๐ฎ Live Demo
๐ Demo: https://liuchuancong.github.io/flame_barrage/
The online demo showcases high-performance barrage rendering, live configuration updates, emoji and sprite-sheet rendering, interaction handling and real-time performance testing.
๐ Overview
FlameBarrage Engine is a high-performance, hardware-accelerated barrage (danmaku) rendering engine built on top of the Flame graphics framework, designed for live streaming platforms, video comment overlays, esports broadcasts and other high-concurrency real-time scenarios.
Instead of a widget per message, the engine renders from a flat entry list through a small system pipeline:
| System | Priority | Responsibility |
|---|---|---|
BarrageDataSystem |
100 | Waiting queue, emit pacing, pre-layout of queued messages, lane allocation on dispatch |
BarrageMotionSystem |
200 | Scroll integration against the logic clock, expiry, immediate swap-remove recycling |
BarrageMetricsSystem |
300 | Per-lane density / avg-speed / youngest-edge aggregation (~30 Hz) |
BarrageRenderSystem |
400 | One flat blit pass over the canvas, plus hit-testing |
All shared state lives in a single BarrageContext (config, logic clock, lanes, pools, caches, live entries). The game loop only runs while there is visible work โ a silent room costs nothing โ and every logic step advances the clock by the real elapsed time, so scroll velocity is uniform regardless of frame load.
๐ฅ Core Features
โก Batch Rendering Pipeline
Text, emoji and graphic assets are decomposed into lightweight rendering fragments and submitted directly to the low-level canvas โ no widgets, no per-message layout, no rebuild churn.
๐ผ๏ธ Rasterized Barrage Bitmaps
Each message is recorded once as a vector display list, then baked into a GPU-resident bitmap at device resolution (BarrageConfig.rasterizeItems, on by default). A display frame draws one textured quad per visible message instead of replaying its text, outline, shadow and emoji operations โ and a stroked glyph run is re-tessellated on the raster thread on every one of those replays, which is what makes a full screen of danmaku miss its deadline on TV-class hardware.
Measured on a 1080p scene with ~74 messages on screen (Windows / Impeller, per-frame raster time): p50 1.99 ms โ 0.82 ms, p90 2.19 ms โ 1.04 ms, VRAM cost 8.3 MB. A synthetic CPU-raster scene of 100 distinct CJK messages drops from 4.63 ms to 2.41 ms per frame.
- Bitmaps are reference counted โ a cache eviction never disposes something still on screen
- Bounded by
pictureCacheMaxSizeentries andrasterCacheMaxBytesof GPU memory - Repeated content ("666", welcome templates) shares a single bitmap across every instance
- Messages longer than 4096 device pixels, and platforms without picture rasterization, transparently keep the vector path
๐จ Dual-Pass Typography Rendering
Text outlines and fills are rendered as independent paragraphs, avoiding font-cache interference: no first-frame dark artifacts, no outline bleeding, no flicker.
โป๏ธ Zero-Allocation Object Pool
BarragePool reuses pooled entry objects; expired entries leave the screen the same frame they expire and return to the pool โ extremely low GC pressure, stable memory over long sessions.
๐ Live Configuration Restyling
updateConfig applies changes without clearing the screen. When a change affects what on-screen messages look like or where they sit, every visible message is re-laid out and re-baked immediately: scrolling messages keep their position, pinned messages re-center under the new lane geometry.
๐ฏ Frame-Rate Independent Motion
Positions integrate against a sub-millisecond logic clock using the real elapsed time of each frame โ velocity never depends on frame load or clamped step durations. Uniform scrolling at 60/120/144 Hz panels, with lane conflict avoidance and safe-gap control.
โฉ Playback Rate & Realtime Dispatch
engine.playbackRatescales the whole logic clock (0.05โ8.0): scroll travel and pinned dwell time together.BarrageConfig.realtimeModebypasses emit pacing and drains the waiting queue every logic frame (still bounded bymaxVisibleCountand lane availability) โ bursts appear on arrival.
๐ฆ Installation
dependencies:
flame: ^1.38.2
flame_barrage: ^0.0.8
๐ Quick Start
import 'package:flutter/material.dart';
import 'package:flame_barrage/flame_barrage.dart';
class VideoPlayerView extends StatefulWidget {
const VideoPlayerView({super.key});
@override
State<VideoPlayerView> createState() => _VideoPlayerViewState();
}
class _VideoPlayerViewState extends State<VideoPlayerView> {
final BarrageController _controller = BarrageController();
final BarrageConfig _config = const BarrageConfig(
fontSize: 20,
baseSpeed: 150,
trackHeight: 44,
showStroke: true,
safeArea: true,
opacity: 0.8,
);
@override
void dispose() {
_controller.clear();
_controller.detach();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: FlameBarrageWidget(
controller: _controller,
config: _config,
emojiAtlas: EmojiAtlas.instance,
),
),
],
);
}
}
Send a message, update config live, pause/resume:
_controller.send(BarrageItem(content: 'Hello FlameBarrage!', type: BarrageType.scroll));
_controller.updateConfig(_config.copyWith(fontSize: 24)); // on-screen messages restyle instantly
_controller.pause();
_controller.resume();
_controller.clear();
๐ API Reference
FlameBarrageWidget
The entry widget. Owns one BarrageEngine instance for its lifetime.
| Parameter | Type | Default | Description |
|---|---|---|---|
controller |
BarrageController |
required | Facade used to send messages and drive the engine |
config |
BarrageConfig |
required | Engine configuration; changes are hot-applied |
emojiAtlas |
EmojiAtlas |
required | Emoji/sprite registry, typically EmojiAtlas.instance |
enablePointerEvents |
bool |
false |
When true, taps/long-presses are hit-tested against barrage items. Without it the surface ignores pointers, so it never steals gestures from an underlying video player |
BarrageController
Type-safe facade between host code and the engine.
| Member | Signature | Description |
|---|---|---|
send |
void send(BarrageItem item) |
Enqueue a message (no-op while running == false) |
updateConfig |
void updateConfig(BarrageConfig config) |
Hot-apply configuration |
pause / resume |
void pause() / void resume() |
Stop/advance the engine; messages sent while paused buffer instead of dropping |
togglePause |
void togglePause() |
Flip running |
clear |
void clear() |
Release every entry, queue and cached artifact |
attach / detach |
void attach(BarrageEngineApi engine) / void detach([BarrageEngineApi? engine]) |
Bind/unbind the engine (done automatically by FlameBarrageWidget) |
triggerItemAt |
bool triggerItemAt(double x, double y, {required bool longPress}) |
Hit-test a point and fire the top-most matching message's callback; returns false when nothing hit |
running |
bool |
Whether sends and engine advancement are active |
totalEmitted |
int |
Messages accepted by send |
activeItemCount |
int |
Messages currently on screen |
pendingMessageCount |
int |
Messages waiting for a lane (incl. pause buffer) |
pictureCacheCount |
int |
Entries in the render cache |
rasterCacheBytes |
int |
GPU bytes held by baked bitmaps |
poolObjectCount |
int |
Entry objects parked in the pool |
BarrageEngine / BarrageEngineApi
BarrageEngine extends FlameGame implements BarrageEngineApi. You normally never construct it directly โ FlameBarrageWidget does. Use it for playback control and metrics:
engine.playbackRate = 2.0; // 0.05โ8.0, scales the whole logic clock
engine.activeCount; // on-screen messages
engine.pendingMessageCount; // queued messages
engine.rasterCacheBytes; // bitmap VRAM usage
engine.activeCacheSize; // render cache entries
engine.activePoolSize; // pooled entry objects
engine.rasterizationActive; // whether bitmaps are being blitted
engine.lastStepCostUs; // last logic-step cost (ยตs)
engine.peakStepCostUs; // peak step cost since last overload warning
engine.parserCacheSize; // parsed content cache entries
engine.layoutCacheSize; // layout result cache entries
engine.frameStepCount; // total logic steps executed
engine.context; // advanced: shared BarrageContext
A logic step exceeding 20 ms logs a throttled overload warning.
BarrageItem
The immutable message description.
| Field | Type | Description |
|---|---|---|
content |
String (required) |
Raw text; may embed emoji keys resolved by the atlas regex |
type |
BarrageType |
scroll (default) / topFixed / bottomFixed |
priority |
int |
Higher wins contested lane allocation |
userId / userName |
String? |
Sender identity |
textColor, fontSize, fontWeight, fontStyle, fontFamily, letterSpacing |
overrides | Per-message typography (falls back to BarrageConfig) |
showStroke, strokeColor, strokeWidth |
overrides | Per-message outline |
showShadow, shadowColor, shadowBlur, shadowOffset |
overrides | Per-message shadow |
opacity |
double? |
Per-message alpha |
fixedDuration |
Duration? |
Per-message dwell time for pinned types |
emojiSize |
double? |
Per-message inline emoji size |
baseSpeed |
double? |
Per-message scroll speed |
overlapSafeGap |
double? |
Per-message lane safe gap |
effect |
BarrageMotionEffect? |
Hands the message to a choreographed mount effect (entrance/cruise/exit, hand-drawn mount, particles); it owns a lane for the whole show |
cachedFragments, cachedLayout, cachedPicture |
advanced | Precomputed artifacts that skip the parse/layout/cache stages |
onTapDown / onTapUp / onLongTapDown / onTapCancel |
void Function()? |
Interaction callbacks (require enablePointerEvents: true or triggerItemAt) |
BarrageConfig
All engine tuning. Every field is hot-appliable via copyWith; style and lane-geometry changes restyle the screen instantly.
All fields
| Group | Field | Type / Default | Description |
|---|---|---|---|
| Typography | fontSize |
double / 18 |
Baseline font size |
fontWeight |
FontWeight / w500 |
Baseline weight | |
fontStyle |
FontStyle / normal |
Italic or normal | |
fontFamily |
String? |
Custom font family | |
letterSpacing |
double / 0 |
Extra glyph spacing | |
| Color & effects | textColor |
Color / white |
Fill color |
showStroke |
bool / true |
Outline rendering | |
strokeColor / strokeWidth |
black / 1.0 | Outline look | |
showShadow / shadowColor / shadowBlur / shadowOffset |
false / black / 0 / (1,1) | Paragraph shadow (no saveLayer per frame) | |
opacity |
double / 1.0 |
Global alpha | |
| Layout | area |
double / 1.0 |
Fraction of viewport height usable for lanes |
trackHeight |
double / 36 |
Lane height (auto-floored to fontSize+10) | |
topAreaDistance / bottomAreaDistance |
0 / 0 | Extra insets beyond the safe area | |
safeArea |
bool / true |
Respect MediaQuery insets |
|
| Timing & speed | fps |
int / 60 |
Logic-step ceiling; display refresh bounds it in practice |
fixedDuration |
Duration / 4s |
Dwell time for pinned messages | |
baseSpeed |
double / 120 |
Scroll speed in px/s | |
| Dispatch | emitInterval |
double / 0.1 |
Seconds between paced dispatches |
realtimeMode |
bool / false |
Bypass pacing; flush the queue every logic frame | |
maxVisibleCount |
int / 80 |
On-screen message cap | |
maxPendingCount |
int / 120 |
Queue cap (oldest dropped beyond it) | |
maxPendingAge |
Duration / 5s |
Drop messages older than this before dispatch | |
overlapSafeGap |
double / 40 |
Minimum clearance between messages in a lane | |
| Resources | noEmojiMode |
bool / false |
Render text only, skip all emoji fragments |
barragePoolMaxSize |
int / 150 |
Entry pool size | |
pictureCacheMaxSize |
int / 200 |
Render cache entry cap | |
rasterCacheMaxBytes |
int / 24 MB |
Bitmap VRAM budget | |
rasterizeItems |
bool / true |
Bake messages into GPU bitmaps | |
textCacheMaxSize |
int / 1000 |
Paragraph/layout cache size | |
effectInterceptors |
List<BarrageEffectInterceptor> / [] |
Custom text-effect pipeline |
BarrageType
| Value | Behavior |
|---|---|
scroll |
Enters from the right edge, scrolls left until off-screen |
topFixed |
Pinned to a lane at the top for fixedDuration |
bottomFixed |
Pinned to a lane at the bottom for fixedDuration |
Emoji & Sprite System
final atlas = EmojiAtlas.instance;
// 1. Register emoji metadata (one image can serve several trigger keys)
atlas.registerAll([
EmojiInfo(
id: '29',
asset: 'assets/emoji/29.png',
keys: ['[laugh]', '[happy]'],
sourceType: EmojiSourceType.asset, // asset | atlas | network | animated
width: 24,
height: 24,
),
]);
// 2. Preload images (async; resolves sprites/animations internally)
await atlas.preloadAll();
// 3. Atlas-sliced emoji: register source rects before loading the sheet image
atlas.updateAtlasRects({
'[sliced-a]': const Rect.fromLTWH(0, 0, 96, 96),
'[sliced-b]': const Rect.fromLTWH(96, 0, 96, 96),
});
| Class | Key members | Purpose |
|---|---|---|
EmojiAtlas |
register, registerAll, updateAtlasRects, preloadEmoji, preloadAll, find, image, getStaticSprite, getAnimation, resolveLoadedImage, regex, clear |
Registry mapping trigger keys to images/sprites/animations; builds a match-all RegExp over the keys |
EmojiInfo |
id, asset, keys, sourceType, width, height |
One emoji's metadata |
EmojiSourceType |
asset / atlas / network / animated |
How the asset is interpreted (animated source types build a SpriteAnimation from a horizontal strip) |
AtlasLoader |
loadFromAsset(path, {targetWidth, targetHeight}) |
Decode a Flutter asset into a ui.Image |
SpriteSheet |
getSpriteRect(i), generateAllRects(), srcWidth, srcHeight |
Grid math for slicing a sheet |
The parser splits message content on the atlas regex: plain runs become TextFragment, registered keys become SpriteFragment (atlas source) or EmojiFragment (everything else), so text and graphics mix freely in one message.
Interaction
FlameBarrageWidget(
controller: controller,
config: config,
emojiAtlas: EmojiAtlas.instance,
enablePointerEvents: true, // required for direct hit testing
);
controller.send(BarrageItem(
content: 'Tap me!',
onTapDown: () => debugPrint('down'),
onTapUp: () => debugPrint('up'),
onLongTapDown: () => debugPrint('long-press'),
onTapCancel: () => debugPrint('cancel'),
));
// Custom gesture layer (e.g. a video player keeps swipes):
final handled = controller.triggerItemAt(x, y, longPress: false);
Hit testing runs back-to-front over the live entry list; messages without callbacks never claim the gesture.
Tap-to-hold (action sheet flow)
A common product flow: tap a message to freeze it, present block/report/like actions, then release it to continue scrolling. The engine holds the message in place โ it neither scrolls nor expires while held:
final item = controller.pauseItemAt(x, y); // null when nothing was hit
if (item != null) {
final action = await showActionSheet(item.userId, item.content);
// ... block the user, file a report, or like ...
}
controller.resumeAllPaused(); // held messages continue from the spot they froze at
| Member | Description |
|---|---|
controller.pauseItemAt(x, y) |
Holds the top-most message at the point; returns it (or null on a miss). Held messages keep their position and lane |
controller.resumeAllPaused() |
Releases every held message |
controller.pausedCount |
How many messages are currently held |
Extending Rendering
BarrageEffectInterceptor โ intercept matching messages and substitute a custom LayoutSpan:
class VipRainbowInterceptor extends BarrageEffectInterceptor {
const VipRainbowInterceptor();
@override
bool shouldIntercept(BarrageItem item, BarrageConfig config) =>
item.content.contains('[VIP]');
@override
LayoutSpan createCustomSpan({
required BarrageItem item,
required String text,
required ui.Paragraph paragraph,
required double x, required double y,
required double width, required double height,
required BarrageConfig config,
}) {
return VipRainbowTextLayoutSpan(
x: x, y: y, width: width, height: height,
text: text, paragraph: paragraph, config: config,
);
}
}
class VipRainbowTextLayoutSpan extends TextLayoutSpan {
const VipRainbowTextLayoutSpan({
required super.x, required super.y, required super.width,
required super.height, required super.text, required super.paragraph,
required this.config,
});
final BarrageConfig config;
// Subclasses of TextLayoutSpan should implement copyWithY(double) so
// vertical centering can reposition them.
VipRainbowTextLayoutSpan copyWithY(double newY) => VipRainbowTextLayoutSpan(
x: x, y: newY, width: width, height: height,
text: text, paragraph: paragraph, config: config,
);
@override
void paint(ui.Canvas canvas) {
// gradient fill, outline, badges โ anything a Canvas can draw
}
}
// Wire it up:
final config = BarrageConfig(effectInterceptors: [const VipRainbowInterceptor()]);
Fragment โ the content unit produced by parsing (TextFragment, SpriteFragment, EmojiFragment). You can extend it and feed your own parsing path before layout.
LayoutSpan โ the paint unit inside a LayoutResult. Built-ins: TextLayoutSpan (fill + optional stroke paragraph), EmojiLayoutSpan (image), SpriteLayoutSpan (atlas sprite with optional SpriteAnimationPlayer).
BarrageMotionEffect โ choreographed mount effects. Implement duration, onSpawn, advance, and optionally paint (drawn underneath the text bitmap). The engine owns spawning, per-frame advance, lane reservation and particle rendering (BarrageFxParticleSystem); you only write pose fields (x/y/alpha/rotation/scale) into the provided BarrageFxState each frame. See the bundled effects (HorseRidingEffect, RocketLaunchEffect, โฆ) as references.
Data Protocols
final message = const MessageProtocol().fromWebSocketJson(json); // BarrageItem? from {content, type, vip}
final emojis = const EmojiProtocol().parseRegistryJson(rawList); // List<EmojiInfo> from {id, keys, asset, width, height}
Utilities
| Class | Members | Purpose |
|---|---|---|
FpsMonitor |
start(callback), stop() |
Real display-frame FPS sampling over a sliding window |
Measure |
Measure.profile(label, action) |
Times a closure, warns above 1 ms |
ColorUtil |
fromHex, fromInt, toARGB32 |
Color conversions |
BarrageLogger |
d/w/e |
Tagged logging used across the engine |
Advanced Internals
Exported for custom hosting or experimentation:
| Area | Classes |
|---|---|
| Systems | BarrageDataSystem, BarrageMotionSystem, BarrageMetricsSystem, BarrageRenderSystem |
| Shared state | BarrageContext (config, clock, viewport, lanes, caches, live entries) |
| Scheduler | TrackManager (lane lifecycle), TrackAllocator (lane picking), SpeedStrategy (catch-up-safe speeds) |
| Caches | RenderCache/CachedRender (ref-counted bitmaps), TextCache (paragraphs), PictureCache (legacy), AtlasCache, SpriteCache, PicturePool |
| Pools | BarragePool (entries), ObjectPool<T> (generic Poolable pooling) |
| Render | BaseRenderer, MixedRenderer (picture recording), EmojiRenderer |
| Model | BarrageEntry (pooled runtime state), BarrageTrack, BarrageMessage |
| Clock | EngineClock (now() ms, nowPrecise() sub-ms, scale for playback rate) |
| Effects | GlowEffect, GradientEffect, ShadowEffect, StrokeEffect (paint factories), ComboAnimation, SpriteAnimationPlayer |
๐บ TV & Low-End Device Tuning
| Setting | Recommendation | Why |
|---|---|---|
fps |
Set it to the panel's refresh rate (60 or 120) | The engine can never step more often than the display refreshes. Setting fps: 144 on a 60 Hz TV changes nothing โ if frames still drop, the cost per frame is the problem, not the target rate. |
rasterizeItems |
true (default) |
Removes per-frame text, stroke and shadow rasterization. |
maxVisibleCount |
40โ80 on a weak GPU | This is the number of bitmap blits per frame. |
showStroke |
Keep true if the text sits on bright video; disable if the bake cost itself shows up as a startup spike |
The outline is rasterized once per message rather than per frame. |
trackHeight / area |
A full screen of lanes only helps if the GPU can fill it | Fewer lanes means fewer messages on screen at once. |
pictureCacheMaxSize / rasterCacheMaxBytes |
Fit the bitmap budget to the device | Caps the GPU memory the cache may hold (rasterCacheBytes reports live usage). |
emitInterval |
0.1 for a calm stream, 0.02โ0.05 for a busy one |
Paces how many new messages are shaped and baked per second. Set realtimeMode: true instead when latency matters more than density. |
Calling controller.clear() (or disposing FlameBarrageWidget) stops the game loop entirely, so a paused room costs nothing.
๐ฌ Performance Philosophy
- Minimize allocations โ pooled entries, reused paints, zero-allocation steady-state loops
- Reduce framework overhead โ no widgets per message, one flat render pass
- Maximize GPU utilization โ baked bitmaps, one textured quad per message
- Rasterize each message once, not every frame
- Integrate motion with real elapsed time โ velocity never depends on frame load
๐ License
Released under the MIT License.