Skip to main content

Performance Covenant

Hs2d is Roku-first. New games must treat the renderer path, allocation behavior, collision model, text cache, particle pools, telemetry, and camera/layer model as part of the game contract.

Source References

SourceWhy it matters
../games/hosanna-ui/src/hosanna-game/hosanna2d/game/Hs2dLevelScene.tsCanonical frame template, scene sprite and particle telemetry, stats window, HUD cache hookup.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dWorld.tsHot-path world tick/render, particle layer registration, snapshots, and camera/layer update rules.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dSpritePool.tsFixed sprite pools, batch movement, collision enable/disable, and active sprite stats.
../games/hosanna-ui/src/hosanna-game/hosanna2d/Hs2dParticleLayer.tsFixed-capacity particle layer update and counters.
../games/hosanna-ui/src/hosanna-game/core/Hs2dTextRenderer.tsCached text, prewarming, text stats, text sprites, and floating text systems.
../games/hosanna-ui/src/hosanna-game/collision/GameCollisionManager.tsCollision registration, debug overlays, single/multiple checks, and stats.
../hosanna-ui-game-samples-public/src/hosanna-game-examples/native-shoot-em-up/VerticalShooterLevelScene.tsReference game with feature flags, pooled projectiles, particles, floating text, collision counters, phase timings, and throttled HUD status.

Hard Rules

  • Use Hs2dGame, scenes, worlds, layers, sprite pools, tile layers, cached surfaces, bitmap caches, and asset gates.
  • Do not create a standalone gameplay GameController with raw screen drawing.
  • Do not add per-frame screen.DrawRect, DrawObject, or DrawScaledObject loops for gameplay sprites, large tile sets, bullets, particles, or backgrounds.
  • Do not allocate objects, arrays, closures, bitmaps, regions, promises, or text sprites in steady-state gameplay frames.
  • Do not read Tiled object lists or layer JSON during the frame loop.
  • Do not configure collision regions or create collision IDs in the frame loop.
  • Do not create particle sprites or text sprites in response to gameplay events.
  • Do not rebuild long HUD/debug strings every frame.
  • Do not use browser-only APIs in shared Roku-transpiled game code.
  • If Roku drops below 55 FPS, fix the render path before adding more visuals or features.

Frame Phases

Keep update and render separate:

Accepted gameplay frame separating input and game rules from retained-state synchronization, render, presentation, and optional telemetryAccepted gameplay frame separating input and game rules from retained-state synchronization, render, presentation, and optional telemetry

Update mutates logical state. Render composes existing layer state. If render code changes gameplay state, fix the scene. Hs2dLevelScene already gives new games the fixed hook sequence shown above.

Put gameplay decisions in onUpdate. Put pooled sprite, text, layer visibility, and collision-sprite synchronization in updateSceneSprites or a bounded sync helper called from update before collision checks. Keep onRender to world.render(screen) plus intentional render-phase telemetry.

Pools

Use fixed-capacity pools for:

  • actors
  • enemies
  • projectiles
  • pickups
  • particles
  • floating text
  • dynamic tiles
  • decor sprite groups

Fixed-capacity logical entity slots activating, synchronizing to matching render slots, deactivating, and being reused without frame-time allocationFixed-capacity logical entity slots activating, synchronizing to matching render slots, deactivating, and being reused without frame-time allocation

Pools should allocate during loading or world build. At runtime, mark entries active/inactive, move unused entries offscreen, and reuse existing sprite or text objects.

Pool sizes are part of the game design. Name them:

const PROJECTILE_POOL_SIZE = 12;
const ENEMY_POOL_SIZE = 24;
const PARTICLE_POOL_SIZE = 160;
const FLOATING_SCORE_TEXT_POOL_SIZE = 16;

If a pool fills, decide whether to reuse the oldest entry, drop the spawn/effect, or lower the source rate. Do not grow the pool dynamically in gameplay.

Cached Surfaces

Use cached compositor surfaces for:

  • static maps
  • chunked maps
  • menu panels
  • repeated backgrounds
  • expensive custom world rendering

Dirty work should happen when content changes, not every frame. Per-frame work should usually be a viewport blit or sprite move.

Camera And Culling

Tick one camera once per frame. Layers read it. Use camera culling for sprites, decor, tile windows, and text. Amortize expensive culling or sort work when possible.

Do not let each layer create independent camera math for gameplay. Put shared projection behavior in the world, camera controller, or layer implementation.

Visibility refresh does not need to run on every pixel of camera movement. The vertical shooter refreshes enemy visibility only after enough camera movement or on forced rebuild. Use a named threshold when culling or sorting is expensive.

Text

Use cached text and text sprite pools for HUD and gameplay labels. Direct DrawText is acceptable for loading screens, diagnostics, bounded menus, and short debug overlays.

Gameplay text rules:

  • prewarm common score/combo labels explicitly with Hs2dTextRenderer.prewarmBitmapText(target, items); text-pool prewarm values alone do not rasterize bitmaps
  • keep cache keys stable
  • use Hs2dFloatingTextSystem for damage/score popups
  • use cull bounds for world-space floating labels
  • update long status text once per stats window
  • track text renderer cacheHits, cacheRedraws, and fallbackDraws when tuning

Do not use text as placeholder art for gameplay sprites. Missing sprites should be fixed in the asset bundle.

Collisions

Collision is update-phase work:

  • define shapes during region/sprite setup
  • use Hs2dCollisionBridge with Hs2d sprites and pools
  • use stable collision IDs
  • sync native sprite positions before checks
  • use checkFirst when one hit resolves the interaction
  • keep tile/map collision as direct logical probes when that is cheaper
  • use collision counters and available debug overlays to tune shapes and check volume; runtime overlay rendering is __DEV__-only, and ordinary Hs2d sprite registrations currently omit debug geometry

Do not put collision checks in render, drawHud, layer render, layer compose, or cached-surface painting.

Particles

Particles must be fixed-capacity sprite particles:

  • add particle layers during world build
  • use CompositorParticleSystem through Hs2dParticleLayer
  • update emitters before super.updateParticles(deltaMs)
  • keep burst sizes named and bounded
  • use activeParticles, emittedParticles, and movedParticles counters
  • clear/reset counters in a stats window

Do not use the removed ParticleEmitter draw2d path. It throws by design.

Asset Work

Do not load assets, create scaled bitmaps, create regions, or parse levels in gameplay frames. Use asset bundles, GameBitmapCache, Hs2dAssetGate, and build-time world construction.

Telemetry

Use Hs2dTelemetry and runtime stats to identify the slow phase before changing code. Track:

  • scene update time
  • sprite sync time
  • particle update time
  • world render time
  • active sprite counts
  • active tile counts
  • active particle/text counts
  • collision checks and hits
  • FPS windows

Performance fixes should reduce measured work in a named phase.

Instrument the phase boundary you can change:

this.markPhaseTimer();
this.checkProjectileHits();
if (__TELEMETRY__) this.telemetry.accumulatePhase('projectileHit');

Then report it on a stats window:

this.statusText =
`fps: ${this.fps}`
+ ` projHitMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('projectileHit'))}`
+ ` spriteMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('sceneSprite'))}`
+ ` particleMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('particleUpdate'))}`
+ ` renderMs: ${this.formatPerfMs(this.telemetry.getAccumulatedMs('render'))}`;

Gate timing setup and accumulation with __TELEMETRY__. Treat zero telemetry from an unavailable timer as "not measured", not as proof of no cost, and never put required game-state work in a telemetry-only stats hook.

Do And Don't

DoDon't
Build worlds, regions, surfaces, pools, and particle layers during loading/build.Create render resources during steady-state gameplay.
Keep update and render phases separate.Mutate gameplay state from render or HUD draw code.
Use fixed-capacity pools and named budgets.Let bullets, particles, labels, or enemies grow unbounded.
Use Hs2d layers and cached surfaces for repeated composition.Add direct screen draw loops for gameplay entities.
Sync collision sprites before checks.Run collisions from render or against stale native sprite positions.
Use cached text and stats-window HUD strings.Re-rasterize long debug text every frame.
Gate expensive features while measuring.Optimize by disabling random systems without phase evidence.
Pair telemetry with active counts.Compare FPS alone and guess the cause.
Leave unrelated files alone during docs-only work.Mix docs expansion with navigation or code refactors unless assigned.
Talk to us