← Browse

@lvcabral/brs-engine

BrightScript Engine - AI Agent Instructions

instructionscopilot

Install

agr install @lvcabral/brs-engine --target copilot

Writes 1 file into .github/copilot-instructions.md, pinned to git-f9a5a8d1.

  • .github/copilot-instructions.md

Document

BrightScript Engine - AI Agent Instructions

Project Overview

brs-engine is a TypeScript-based interpreter for Roku's BrightScript language that runs on browsers, Node.js, and Electron. It simulates Roku's runtime environment, including the Draw 2D API and SceneGraph framework, enabling Roku app development and testing outside of Roku hardware.

Key Architecture:

  • Monorepo structure with three packages: packages/browser (web/Electron) published as brs-engine, packages/node (CLI/server) published as brs-node, and packages/scenegraph published as brs-scenegraph
  • Web Worker architecture: Browser package runs interpreter in a Web Worker (brs.worker.js) with API library (brs.api.js) for host communication
  • Lexer → Parser → Interpreter pipeline: Code flows through src/core/lexersrc/core/parsersrc/core/interpreter
  • Component System: BrightScript objects live in src/core/brsTypes/components; SceneGraph nodes live in src/extensions/scenegraph/nodes
  • Extension System: SceneGraph (and other future features) are plug-in extensions implementing BrsExtension interface (src/core/extensions.ts); registered with registerExtension()
  • Device Simulation: BrsDevice (in src/core/device/) maintains simulated Roku device state (registry, file system, device info)
  • Roku OS Version: Currently synchronized with Roku OS 15.x features and APIs

Recent Major Improvements

SceneGraph Extension Architecture (v2.x)

  • Third package: packages/scenegraph (published as brs-scenegraph v0.1.0+) is a standalone addon that owns the XML component parser, RoSGScreen, all nodes, and Task execution helpers
  • Extension system: BrsExtension interface (src/core/extensions.ts) is the plug-in contract; registerExtension() wires extensions into every new interpreter instance without forking the core
  • BrightScriptExtension class: The entry point for the SceneGraph addon (src/extensions/scenegraph/index.ts), implements BrsExtension
  • SGRoot singleton (src/extensions/scenegraph/SGRoot.ts): Holds the active scene, m.global, focus, threads, timers, and animations
  • SupportedExtension enum: SupportedExtension.SceneGraph ("brs-scenegraph"), SDK1 and BrightSign extensions are planned

SceneGraph Rendezvous & Threads (v2.1.0)

  • Real Rendezvous: SceneGraph now implements thread updates similar to Roku's Rendezvous pattern for safe cross-thread field access
  • Multiple roMessagePort in Main thread: Tasks and Main thread can each own multiple ports
  • Task debugger support: Task threads can be paused at breakpoints via the MicroDebugger

RoSGNode Refactoring & Field System

  • Abstract base class: RoSGNode is abstract; all nodes must extend either RoSGNode or the concrete Node class
  • Field type validation: Field.canAcceptValue() validates typed arrays (intarray, floatarray, boolarray, stringarray, colorarray, timearray) and converts string values to the correct type
  • Field aliases: Nodes support multiple comma-separated field aliases; aliases fire observers correctly and support child change propagation
  • System fields are protected: Cannot be removed via removeField() or added via setFields(); use addFields() for new fields
  • setValue vs setValueSilent: setValue() triggers observers; setValueSilent() does not (used during initialization)
  • setValue() signature: Does NOT create a field if it does not already exist (prevents accidental field creation on assignment)

New SceneGraph Nodes (v2.0 – v2.1)

  • Animation system: Animation, ParallelAnimation, SequentialAnimation, FloatFieldInterpolator, ColorFieldInterpolator, Vector2DFieldInterpolator
  • Panel nodes: PanelSet, Panel, ListPanel, GridPanel, OverhangPanelSetScene, Overhang
  • Standard dialogs: StandardKeyboardDialog, StandardProgressDialog, StdDlgContentArea, StdDlgProgressItem, StdDlgTitleArea
  • Input nodes: PinPad, VoiceTextEditBox, ScrollableText, ScrollingLabel
  • Grid nodes: PosterGrid, MaskGroup
  • Other: InfoPane, RSGPalette, ChannelStore

Content Handling Pattern

  • Standardized content caching: All list/grid nodes follow the refreshContent() pattern
  • setValue override: Content fields processed in setValue(), triggering refreshContent()
  • Performance optimization: Content cached once, never re-fetched per frame

File System Improvements

  • Case preservation: Writeable volumes (tmp:, cachefs:) preserve original case
  • Shared volumes: tmp: and cachefs: are shared among threads
  • Pinned @zenfs/core v2.5.0: Using synchronous config for reliability

Type System Enhancements

  • Uninitialized validation: Raises type mismatch error when passing Uninitialized to non-dynamic function parameters
  • Double support: Added d flag to ParseJson() for parsing to double type
  • Type coercion: Typed functions return 0 when no return statement is hit (user functions only)
  • UTF-8 BOM support: Lexer now correctly handles files with UTF-8 BOM
  • DRM detection: roDeviceInfo.getDrmInfoEx() populated in browser environments

API and Library Improvements

  • Worker as Blob: Browser API supports loading the Worker bundle from a Blob
  • Log level management: Device data and interpreter output now have configurable log levels
  • LexerParser module: Separated lexer and parser into reusable module
  • Screenshot API: getScreenshot() method returns full-resolution ImageData

Development Workflows

Build & Test

npm run build          # Build all three packages (outputs to packages/*/lib and packages/*/bin)
npm run build:web      # Build browser + scenegraph packages and launch dev server
npm run build:node     # Build Node.js package only
npm run build:cli      # Build Node.js + scenegraph packages
npm run build:sg       # Build scenegraph extension package only
npm run test           # Run Vitest tests
npm run start          # Start webpack dev server for browser package

Key Build Details

  • Webpack bundles TypeScript using ts-loader with separate configs per package
  • ifdef-loader enables conditional compilation with /// #if BROWSER, /// #if DEBUG, /// #else, /// #endif directives
  • Browser package creates two bundles: brs.worker.js (interpreter) and brs.api.js (host API)
  • Node package creates three: brs.cli.js (CLI), brs.ecp.js (ECP server), brs.node.js (library)
  • SceneGraph package creates two bundles: brs-sg.js (browser/Worker) and brs-sg.node.js (Node.js library)
  • SceneGraph build also assembles assets/common.zip (fonts, locale, images for common:/ volume) and copies it to the browser and node packages
  • Lint/format gate: run npm run lint and npm run prettier:write before committing; both must be clean. ESLint enforces no-case-declarations — when a switch case/default declares a binding (let/const/function/class), wrap that case body in braces (case X: { const y = ...; break; }) so the binding doesn't leak into sibling cases. ESLint also enforces unicorn/prefer-string-raw: use String.raw`...` instead of string literals with escaped backslashes ("C:\\folder"String.raw`C:\folder`, "\\d+"String.raw`\d+`) — auto-fixable with eslint --fix. It only targets plain string literals (template literals and a lone trailing "\\" are left alone). And unicorn/prefer-string-replace-all: use .replaceAll(/…/g, …) instead of .replace(/…/g, …) for global replacements (also auto-fixable; the g flag stays because replaceAll requires it).

Running the CLI

# After building
./packages/node/bin/brs.cli.js path/to/app.zip
./packages/node/bin/brs.cli.js path/to/script.brs

Code Patterns & Conventions

BrightScript Type System

  • All BrightScript values implement BrsType from src/core/brsTypes/BrsType.ts
  • Primitive types: BrsBoolean, BrsString, Int32, Int64, Float, Double in src/core/brsTypes/
  • Components (Roku objects like roArray, roAssociativeArray): Extend BrsComponent in src/core/brsTypes/components/
  • Nodes (SceneGraph like Group, RowList): Extend base node classes in src/extensions/scenegraph/nodes/
  • Callable functions: Wrap native TypeScript functions using Callable class from src/core/brsTypes/Callable.ts

Example adding a stdlib function:

export const MyFunction = new Callable("myFunction", {
    signature: {
        args: [{ name: "input", type: ValueKind.String }],
        returns: ValueKind.String,
    },
    impl: (_interpreter, input: BrsString) => {
        return new BrsString(input.value.toUpperCase());
    },
});

SceneGraph Node Implementation

  • RoSGNode is now abstract: All SceneGraph nodes extend RoSGNode (abstract) or Node (concrete base class)
  • Fields are declared in defaultFields: FieldModel[] array with name, type, value, optional alwaysNotify, optional hidden
  • Field system improvements: Supports typed arrays (intarray, floatarray, boolarray, stringarray, colorarray, timearray)
  • Lazy per-node allocation (large content trees): hidden default fields (ContentNode metadata) are not materialized up front — they live in a shared per-class spec and are built on first access via resolveField/hasNodeField; a node's ~70 method Callables are built on demand via BrsComponent.buildMethods()/ensureMethods() (RoSGNode methods are prototype getters, the per-node RoHttpAgent is lazy). Use resolveField/hasNodeField for by-name lookups that must see hidden metadata, and never treat a method getter as an identity-stable field. See the "Per-node memory" section of .claude/CLAUDE.md. Coverage: test/extensions/scenegraph/{HiddenFields,LazyMethods}.test.js
  • System fields are protected: Cannot be removed via removeField() or added via setFields(); use addFields() for new fields
  • setValue vs setValueSilent: setValue() triggers observers, setValueSilent() does not (used during initialization)
  • Custom rendering overrides renderNode() method, receives IfDraw2D context
  • Field observers use observeField() to watch changes
  • Node lifecycle: init() called on creation, deinit() on destruction
  • See src/extensions/scenegraph/nodes/RowList.ts and src/extensions/scenegraph/nodes/ZoomRowList.ts as complex examples with focus handling and child rendering

SceneGraph Rendering Architecture

Node Hierarchy and Base Classes

All SceneGraph nodes inherit from RoSGNode (src/extensions/scenegraph/components/RoSGNode.ts), which provides:

  • Field management: Dynamic field registration with Field class, aliases, observers, and notifications
  • Child management: Parent-child relationships via ifSGNodeChildren interface (appendChild, removeChild, etc.)
  • Focus system: Focus chain tracking via ifSGNodeFocus (hasFocus, setFocus, isInFocusChain)
  • Bounding rectangles: Three coordinate spaces tracked in every node:
    • rectLocal: Node's own coordinate space (relative to itself)
    • rectToParent: Transformed to parent's coordinate space
    • rectToScene: Transformed to root Scene coordinate space
  • Standard fields: All nodes have id, focusedChild, focusable, change fields by default

Group (src/extensions/scenegraph/nodes/Group.ts) extends Node (which extends RoSGNode) and is the base for all visual/renderable nodes:

  • Transform fields: translation [x,y], rotation (degrees), scale [x,y], scaleRotateCenter [x,y], opacity (0-1), visible (boolean)
  • Layout fields: width, height, clippingRect for cropping child rendering
  • Child rendering: renderChildren() recursively renders child nodes with inherited transforms
  • Drawing utilities: Helper methods for text (drawText, drawTextWrap, breakTextIntoLines, ellipsizeLine) and images (drawImage, loadBitmap)
  • Coordinate transforms: inheritParentTransform and inheritParentOpacity apply parent values to children
  • Caching: isDirty flag tracks when text measurements need recalculation, cachedLines stores text layout

Node Type Categories

  1. Container Nodes (manage children, no direct visual output):

    • Group: Basic container with transforms
    • LayoutGroup: Auto-layout children in rows/columns
    • Scene: Root node, sets screen resolution (SD/HD/FHD), background color/image, dialog management
  2. Visual Leaf Nodes (render content, usually have no children):

    • Label: Single/multi-line text with alignment, wrapping, ellipsization
    • Poster: Image display with scaling modes (noScale, scaleToFit, scaleToZoom), 9-patch support
    • Rectangle: Filled rectangle with color and optional rotation
    • BusySpinner: Animated loading indicator
  3. Interactive Container Nodes (visual + focus + children):

    • ArrayGrid: Grid of items with focus management and scrolling (base for grids)
    • RowList: Horizontal rows of items, each row is a scrollable list (fully implemented with row titles, focus feedback)
    • ZoomRowList: Advanced row list with zoom animations and configurable row heights
    • PosterGrid: Grid of poster images with focus handling
    • LayoutGroup: Auto-layout container with horizontal/vertical arrangement and alignment
    • MarkupList/MarkupGrid: Similar to above with markup text support
    • LabelList, CheckList, RadioButtonList: Specialized list types
    • ButtonGroup, Button: Interactive button controls
    • Keyboard, MiniKeyboard, TextEditBox, PinPad, VoiceTextEditBox: Input controls
    • PanelSet, Panel, ListPanel, GridPanel: Panel-based navigation layout
  4. Animation Nodes:

    • Animation, ParallelAnimation, SequentialAnimation: Animation container nodes
    • FloatFieldInterpolator, ColorFieldInterpolator, Vector2DFieldInterpolator: Field interpolators
  5. Special Nodes:

    • ContentNode: Data-only node (no rendering), holds metadata for lists/grids
    • Task: Background thread execution (runs BrightScript in separate Worker)
    • Timer: Interval/timeout events
    • Video, Audio, SoundEffect: Media playback
    • Dialog, KeyboardDialog, StandardDialog, StandardKeyboardDialog, StandardProgressDialog: Modal overlays
    • Font: Font resource definition
    • RSGPalette: Color palette resource
    • ChannelStore: In-channel purchasing
    • MaskGroup: Masked rendering group
    • InfoPane, Overhang, OverhangPanelSetScene: UI chrome nodes
    • ScrollingLabel, ScrollableText: Scrolling text nodes

Rendering Pipeline and Flow

SceneGraph Rendering Architecture Overview: The SceneGraph rendering system is triggered by roSGScreen (the display component) and flows through a hierarchy of renderNode() method calls. The entry point is always Scene.renderNode(), which then recursively calls renderNode() on child nodes. This is fundamentally different from a typical DOM-based rendering system - nodes must explicitly implement renderNode() to participate in the rendering pipeline.

Key Principle: Nodes that want custom rendering behavior MUST override renderNode(), not invent new methods like renderContent(). Group's base renderNode() only calls renderChildren() - it has no concept of "content rendering".

Initialization (SceneGraph bootstrap):

  1. Component XML parsing (src/extensions/scenegraph/parser/ComponentDefinition.ts):
    • Scan pkg:/components/ for .xml files
    • Parse XML with xmldoc library into ComponentDefinition objects
    • Build inheritance tree (components can extend other components or built-in types)
    • Extract <interface> (fields/functions), <children> (initial child nodes), <script> tags
  2. Node factory (src/extensions/scenegraph/factory/NodeFactory.ts):
    • createNode() instantiates nodes from string type names
    • Built-in types registered in NodeFactory.ts switch statement
    • Custom components use ComponentDefinition to create nodes with inherited fields
  3. Environment setup:
    • Each component gets its own Environment (scope) for BrightScript functions
    • init() function called after node creation if defined in component script
    • Field observers registered, initial field values set

Frame Render Cycle (triggered by roSGScreen display update):

  1. roSGScreen.renderFrame() initiates the cycle:

    • Gets the Scene node from sgRoot.scene
    • Calls scene.renderNode(interpreter, [0, 0], 0, 1.0, draw2D)
    • This is the ONLY entry point to the rendering system
  2. Scene.renderNode() called with:

    • interpreter: Active interpreter instance
    • origin: [x, y] position in screen coordinates (starts at [0, 0])
    • angle: Accumulated rotation from parent chain (starts at 0)
    • opacity: Accumulated opacity from parent chain (starts at 1.0)
    • draw2D: IfDraw2D interface for canvas drawing
  3. Scene-specific rendering:

    • Clears canvas with backgroundColor
    • Draws backgroundURI image if set (scaled to screen resolution)
    • Calls renderChildren() to process child nodes
  4. Group.renderNode() recursion (for each child):

    • Visibility check: Skip if visible field is false (ALWAYS check this first in custom renderNode)
    • Transform calculation:
      • Get node's translation field: nodeTrans = this.getTranslation()
      • Calculate draw position: drawTrans = [nodeTrans[0] + origin[0], nodeTrans[1] + origin[1]]
      • If parent has angle, rotate translation vector: rotateTranslation(nodeTrans, angle)
    • Accumulate transforms:
      • rotation = parentAngle + this.getRotation()
      • opacity = parentOpacity * this.getOpacity()
    • Custom node rendering (nodes override renderNode for this):
      • Simple visual nodes (Label, Poster, Rectangle): Call IfDraw2D methods directly
      • Complex nodes (ArrayGrid, RowList, ZoomRowList): Implement full custom rendering logic
      • Container nodes: Just call renderChildren() to delegate to children
    • Bounding rect updates:
      • updateBoundingRects(rect, origin, rotation): Updates rectLocal, rectToParent, rectToScene
      • Used for hit testing, collision detection, debugging
    • Recurse to children: renderChildren(interpreter, drawTrans, rotation, opacity, draw2D)
    • Parent rect propagation: updateParentRects(origin, angle) updates parent's bounding rects

Key Rendering Concepts:

  • Coordinate space transformations: Every node maintains three rect representations for different use cases (local calculations, parent-relative layout, screen-absolute hit testing)
  • Transform inheritance: Children accumulate parent transforms (translation, rotation, opacity) at render time
  • Depth-first traversal: Parents render before children, ensuring proper z-ordering
  • Canvas-based drawing: All drawing operations use HTML5 Canvas 2D context via IfDraw2D interface
  • Lazy evaluation: Bounding rects and transforms calculated during render pass, not on field changes

Implementing Custom renderNode(): When creating a custom node that needs to render visual content, follow this pattern (see ArrayGrid.ts, RowList.ts, ZoomRowList.ts):

renderNode(interpreter: Interpreter, origin: number[], angle: number, opacity: number, draw2D?: IfDraw2D) {
    // 1. ALWAYS check visibility first
    if (!this.isVisible()) {
        return;
    }
    
    // 2. Calculate transforms
    const nodeTrans = this.getTranslation();
    const drawTrans = nodeTrans.slice();
    drawTrans[0] += origin[0];
    drawTrans[1] += origin[1];
    const rotation = angle + this.getRotation();
    opacity = opacity * this.getOpacity();
    
    // 3. Do your custom rendering here
    // - Use draw2D methods for drawing
    // - Access cached data (e.g., this.content)
    // - Create/update item components
    // - Call itemComp.renderNode() for child items
    
    // 4. Update bounding rectangles
    this.rectToScene = { x: drawTrans[0], y: drawTrans[1], width: ..., height: ... };
    this.rectToParent = { x: nodeTrans[0], y: nodeTrans[1], width: ..., height: ... };
    
    // 5. Render children (if any non-content children exist)
    this.renderChildren(interpreter, drawTrans, rotation, opacity, draw2D);
    
    // 6. Update parent rects and mark clean
    this.updateParentRects(origin, angle);
    this.isDirty = false;
}

Common Mistakes to Avoid:

  • ❌ Creating methods like renderContent() - Group doesn't call them
  • ❌ Calling getFieldValue() repeatedly in render loop - cache data in refreshContent() or setValue()
  • ❌ Forgetting visibility check - causes rendering of invisible nodes
  • ❌ Not calling updateParentRects() and setting isDirty = false - breaks bounding box calculations
  • ❌ Not handling the case when content is empty - can cause crashes

Drawing Interface (IfDraw2D)

IfDraw2D interface (src/core/brsTypes/interfaces/IfDraw2D.ts) provides BrightScript ifDraw2D API:

  • Canvas management: doClearCanvas(), getContext(), getCanvas(), getRgbaCanvas()
  • Basic shapes: doDrawLine(), doDrawPoint(), doDrawRect(), doDrawRotatedRect()
  • Text rendering: doDrawText() with font, color, alignment, rotation support
  • Image drawing:
    • doDrawObject(): Draw bitmap at position
    • doDrawScaledObject(): Draw with scale factors
    • doDrawRotatedObject(): Draw with rotation around center point
    • doDrawTransformedObject(): Combined scale + rotation + translation
    • doDrawCroppedBitmap(): Draw portion of bitmap (for sprites, tiling)
  • Collision detection: collision() helper for RectRect, RectCircle, CircleCircle

BrsDraw2D Components (implement IfDraw2D for off-screen rendering):

  • RoBitmap (src/core/brsTypes/components/RoBitmap.ts): In-memory image with alpha channel, supports 9-patch borders
  • RoRegion (src/core/brsTypes/components/RoRegion.ts): Sub-region of bitmap for sprite sheets, tiling
  • RoScreen (src/core/brsTypes/components/RoScreen.ts): Double-buffered main screen, SwapBuffers for frame display
  • RoCompositor (src/core/brsTypes/components/RoCompositor.ts): Layer compositor with sprites, z-ordering, collision

Canvas Pooling: createNewCanvas() and releaseCanvas() manage reusable canvas contexts to avoid GC pressure

Content Handling Pattern

ArrayGrid/RowList/ZoomRowList Content Processing: Nodes that display dynamic content from ContentNode trees follow this pattern:

  1. Content Field in setValue() Method (note: set() method is deprecated, use setValue()):
setValue(index: string, value: BrsType, alwaysNotify?: boolean, kind?: FieldKind, sync?: boolean) {
    const fieldName = index.toLowerCase();
    
    if (fieldName === "content") {
        // First, store the field value
        super.setValue(index, value, alwaysNotify, kind);
        
        // Clear existing item components
        this.itemComps.length = 0; // or this.rowItemComps.length = 0
        
        // Process content into cache
        this.refreshContent();
        
        // Set initial focus if needed
        if (this.content.length > 0 && this.focusIndex < 0) {
            this.focusIndex = 0;
        }
        
        return;
    }
    
    super.setValue(index, value, alwaysNotify, kind, sync);
}
  1. refreshContent() Method:
protected refreshContent() {
    // Clear content cache
    this.content.length = 0;
    
    // Get content field value
    const contentNode = this.getFieldValue("content");
    if (!(contentNode instanceof ContentNode)) {
        return;
    }
    
    // Extract children into flat array
    const children = contentNode.getNodeChildren();
    this.content = children.filter((child) => child instanceof ContentNode) as ContentNode[];
    
    // Initialize tracking arrays (focus, scroll, etc.)
    for (let i = 0; i < this.content.length; i++) {
        this.rowFocus[i] = this.rowFocus[i] ?? 0;
        this.rowScrollOffset[i] = this.rowScrollOffset[i] ?? 0;
    }
}
  1. Use Cached Content in renderNode():
renderNode(...) {
    // Use cached content, NOT getFieldValue("content")
    if (this.content.length === 0) {
        return;
    }
    
    for (let i = 0; i < this.content.length; i++) {
        const contentItem = this.content[i]; // Already a ContentNode
        // Render using cached data
    }
}

Key Points:

  • ALWAYS call super.setValue() first to store the field value
  • NEVER call getFieldValue("content") or getNodeChildren() in render methods - use cached this.content array
  • Process content once in refreshContent(), use cache everywhere else
  • This prevents infinite loops and improves performance
  • The setValue() method is the modern approach; set() is deprecated but maintained for compatibility

Performance and Caching

  • Text measurement caching: Group.isDirty + cachedLines avoid re-measuring text on every frame
  • Bitmap texture management: TextureManager (global singleton) caches loaded images by URI; a separate global registry in src/core/device/Graphics.ts (gated by BrsDevice.tracking) tracks all live bitmaps/fonts for the r2d2-bitmaps debug query
  • Lazy bounding rect updates: Only recalculated during render pass when transforms change
  • Conditional rendering: Nodes check visible field early to skip invisible subtrees
  • Transform accumulation: Transforms calculated incrementally down tree, not recalculated from root

Component Lifecycle

  1. Creation: createNode() or createChild() instantiates node, sets initial fields
  2. Initialization: init() BrightScript function called if defined in component
  3. Field changes: Observers notified, onChange callbacks invoked
  4. Rendering: renderNode() called every frame if visible
  5. Focus changes: onKeyEvent() called when node has focus and receives key press
  6. Destruction: deinit() called, observers removed, children destroyed recursively

Event Handling

  • Key events: Scene.handleOnKeyEvent() walks focus chain from focused node up to Scene
    • Each node's onKeyEvent() BrightScript function called if defined
    • If returns true, event consumed; if false, bubbles to parent
    • Built-in nodes (like Group) have handleKey() method for default behavior
  • Field observers: observeField() registers callback (function name or message port) for field changes
  • Timer events: Timer node posts messages to message port on interval/timeout

Conditional Compilation

Use preprocessing directives for platform-specific code:

/// #if BROWSER
// Browser-only code (uses Web APIs)
/// #else
// Node.js code (uses Node APIs)
/// #endif

Common pattern: src/core/index.ts uses /// #if BROWSER to set up Worker onmessage vs Node.js postMessage mock

Communication Patterns

  • Browser: Host ↔ Worker via postMessage() with typed payloads (AppPayload, TaskPayload)
  • Shared memory: SharedArrayBuffer for control signals between host and Worker (key events, display state)
  • Events: BrsDevice.sharedArray holds inter-thread communication buffer (see src/core/device/BrsDevice.ts)

File System Architecture

  • Virtual FS: FileSystem class (src/core/device/FileSystem.ts) uses @zenfs/core for in-memory file system
  • Roku volumes: pkg:/, tmp:/, cachefs:/, ext1:/ (external) simulated as mount points
  • Zip packages auto-extracted to pkg:/ on execution

Testing Patterns

  • Vitest tests in test/ mirror source structure
  • End-to-end tests in test/e2e/ run full BrightScript files
  • Simulator tests in test/simulator/ contain .brs files exercising runtime features
  • Use execute(source) helper from interpreter tests to run BrightScript code snippets

Common Tasks

Adding a New BrightScript Component

  1. Create file in src/core/brsTypes/components/Ro<ComponentName>.ts
  2. Extend BrsComponent, implement required interfaces (e.g., IfArray, IfAssociativeArray)
  3. Register in src/core/stdlib/CreateObject.ts factory function
  4. Add tests in test/brsTypes/components/

Adding a New SceneGraph Node

  1. Create file in src/extensions/scenegraph/nodes/<NodeName>.ts
  2. Extend appropriate base (Node, Group, ArrayGrid, etc.)
  3. Define defaultFields with all Roku-documented fields
  4. Implement renderNode() for rendering if visual node
  5. Register in src/extensions/scenegraph/factory/NodeFactory.ts node registry
  6. Export from src/extensions/scenegraph/nodes/index.ts

Working with the Debugger

  • Developer vs production mode: The Micro Debugger and all debug instrumentation are gated behind debugOnCrash (CLI --debug / API options.debugOnCrash), via the BrsDevice.tracking flag. Production is the default. Encrypted .bpk packages always run in production mode.
  • Micro Debugger (developer mode only): Set breakpoints with the STOP statement in BrightScript. In production mode the debugger is disabled, so STOP exits the app (EXIT_BRIGHTSCRIPT_STOP) and break requests are ignored.
  • Debug API: Call debug("break") from host to pause interpreter
  • Console integration: print statements route through BrsDevice.stdout
  • Gating new instrumentation: counters/registries that exist only for debug commands (bscs, sgnodes, stats, the r2d2-bitmaps texture registry) must be gated behind BrsDevice.tracking so they add no overhead in production.

Important Constraints

  • No eval(): BrightScript's Eval() not implemented (documented in limitations.md)
  • Task threads limited to 10 per app (see limitations.md)
  • m.global is shared: Changes to m.global are now properly shared across Task threads
  • Video/Audio lifecycle: Must call .stop() before destroying player objects or playback continues
  • CORS: Web apps need CORS proxy for cross-origin roUrlTransfer calls (configurable in DeviceInfo.corsProxy)
  • System fields protection: Cannot remove system fields or use setFields() to add new fields (use addFields() instead)
  • Field type validation: Field assignment now validates types (e.g., intarray, floatarray) and converts values appropriately

Key Files to Reference

  • Core entry: src/core/index.ts - Interpreter initialization and main execution loop
  • API surface: src/api/index.ts - Browser package's public API (see docs/engine-api.md)
  • Type definitions: src/core/brsTypes/BrsType.ts - Foundation of type system
  • Device state: src/core/device/BrsDevice.ts - Registry, file system, device info singleton
  • Manifest parsing: src/core/common.ts - parseManifest() and device info types
  • Extension contract: src/core/extensions.ts - BrsExtension interface and registerExtension()
  • SceneGraph entry: src/extensions/scenegraph/index.ts - BrightScriptExtension class, exports all SG symbols
  • SGRoot singleton: src/extensions/scenegraph/SGRoot.ts - Active scene, m.global, focus, threads, timers
  • SceneGraph bootstrap: src/extensions/scenegraph/parser/ComponentDefinition.ts - Component XML parsing and node tree building
  • Node factory: src/extensions/scenegraph/factory/NodeFactory.ts - createNode() registry for all built-in node types
  • Serializer: src/extensions/scenegraph/factory/Serializer.ts - Node serialization for cross-thread messaging
  • Lexer/Parser module: src/core/LexerParser.ts - Separated lexer and parser functions for reusability
  • Field system: src/extensions/scenegraph/nodes/Field.ts - Field model, type validation, and conversion logic

Project-Specific Quirks

  • mod keyword conflict: Cannot use mod as variable name (BrightScript operator vs identifier)
  • Memory info: roAppMemoryMonitor only accurate in Node.js and Chromium (uses non-standard performance.memory)
  • Prettier config: Use 4-space tabs, 120 char line width (see package.json)
  • Branch naming: Current development branch is master
  • Platform detection: Use BrsDevice.deviceInfo.customFeatures array to check host-defined capabilities (e.g., "touch_controls")
  • Virtual File System: Uses @zenfs/core with case-insensitive file system; writeable volumes preserve original case
  • Type coercion: Functions automatically convert between Integer and Float types when needed
  • Typed returns: User functions with typed returns automatically return 0 if no return statement is hit
  • SceneGraph CLI flag: The Node.js CLI loads the SceneGraph extension by default; pass --no-sg to skip it
  • Node.js library: Manually register SceneGraph extension with registerExtension(() => new BrightScriptExtension()) from brs-scenegraph

Documentation

  • Limitations: See docs/limitations.md for unsupported features and known issues
  • Customization: See docs/customization.md for DeviceInfo config and manifest options
  • Extensions: See docs/extensions.md for details on the extension system and brs-scenegraph integration
  • Contributing: See docs/contributing.md for PR guidelines

Trust

Not scanned yet. Artifacts are graded after they are crawled, so a recently discovered one may have no result for a while.

Versions

  • git-f9a5a8d19ccb2026-08-04