Scene Composition and obs_scene_item_t Transform Handling in OBS Studio

OBS Studio composes scenes using a doubly-linked list of obs_scene_item structures that encapsulate position, scale, rotation, and cropping data, converting user-friendly parameters into GPU-compatible 4×4 matrices during each render tick.

OBS Studio's scene composition engine manages visual stacking through the obs_scene_item_t type (defined in libobs/obs-scene.h), which wraps individual sources and handles all geometric transformations required to render them correctly on canvas. Understanding how this system converts high-level properties into low-level graphics matrices is essential for plugin developers and contributors working with the obsproject/obs-studio codebase.

Core Data Structures

The scene system relies on four primary structures defined in libobs/obs-scene.h and libobs/graphics/matrix4.h:

  • obs_scene (lines 30–78): Holds the scene source, group flags, custom canvas size, and a pointer to the first scene item. It includes recursive mutexes (video_mutex, audio_mutex) for thread-safe access to the item list.

  • obs_scene_item (lines 30–99): Represents a single element within a scene. Key fields include reference counters (ref, active_refs), visibility flags (visible, user_visible), transform data (pos, scale, rot, alignment), cropping (crop), bounds configuration, blend settings, and prev/next pointers for the linked list.

  • matrix4 (matrix4.h): A 4×4 transformation matrix used for both drawing and selection box calculations.

  • obs_scene_item_crop (lines 52–53): Stores pixel-based crop values applied before rendering.

The Transform Pipeline

When scene_video_render executes in obs-scene.c (lines 59–85), it iterates the linked list and processes each visible item through a seven-stage pipeline:

Update Invalidation and Canvas Scaling

The system first checks whether update_item_transform needs execution. This occurs when update_transforms_and_prune_sources (lines 33–38) detects changes to the item's update_transform flag, source dimensions, or parent scene size. For groups, item_canvas_scale maintains independent scaling relative to the parent canvas, while pos_to_absolute and size_to_absolute convert relative coordinates to absolute pixels when absolute_coordinates is disabled.

Bounds Processing and Alignment

The calculate_bounds_data function (lines 46–98) enforces maximum dimensions and scaling constraints, updating item->bounds_crop when boundaries require cropping. Subsequently, add_alignment (lines 25–36) adjusts the origin point based on alignment flags (top, left, right, bottom, center combinations).

Matrix Construction

The actual transformation matrix assembly occurs in update_item_transform (lines 56–62):

matrix4_identity(&item->draw_transform);
matrix4_scale3f(&item->draw_transform, &item->draw_transform,
                scale.x, scale.y, 1.0f);
matrix4_translate3f(&item->draw_transform,
                &item->draw_transform, -origin.x, -origin.y, 0.0f);
matrix4_rotate_aa4f(&item->draw_transform,
                &item->draw_transform, 0.0f, 0.0f, 1.0f,
                RAD(item->rot));
matrix4_translate3f(&item->draw_transform,
                &item->draw_transform, position.x, position.y, 0.0f);

This sequence first applies scaling, then rotates around the centered origin, and finally translates to the absolute position. The same logic populates item->box_transform, which UI selection boxes use for hit detection.

Rendering and Synchronization

After emitting the "item_transform" signal (lines 98–100) for plugin notifications, render_item (lines 63–84) pushes the matrix onto the graphics stack. It determines whether texture rendering is required via item_texture_enabled (lines 36–40), draws the source or its transition, then restores the previous graphics state. All operations execute under the scene-level mutex (video_lock/video_unlock) to prevent list corruption during concurrent audio, UI, or rendering thread access.

Visibility, Audio Actions, and Blending

Visibility control involves two flags: item->visible (final computed state) and item->user_visible (user toggle). When visibility changes, OBS queues an audio action (struct item_action) that scene_audio_render (lines 13–24) processes to mute or unmute the source's audio buffers per-sample.

Blending modes map through obs_blend_mode_params (lines 62–86), translating high-level enums like OBS_BLEND_NORMAL or OBS_BLEND_ADDITIVE into GPU blend functions applied in render_item_texture (lines 69–74).

Group Scenes and Nested Composition

Groups are scenes marked with is_group set to true. When rendered inside a parent scene, group items maintain their own transform list while inheriting the parent matrix. The resize_group function recomputes child transforms when the parent scene dimensions change, ensuring the group scales as a single unit rather than distorting individual elements.

Practical Implementation Examples

Creating Scenes and Adding Sources

/* Create a new scene source */
struct obs_source *scene_src = obs_source_create("scene", "MyScene", NULL, NULL);

/* Retrieve the internal obs_scene handle */
struct obs_scene *scene = obs_scene_from_source(scene_src);

/* Create and configure a source */
struct obs_source *image_src = obs_source_create(
        "image_source", "Logo", NULL, NULL);
obs_source_set_file(image_src, "/path/to/logo.png");

/* Add to scene - wraps obs_scene_add_internal (obs-scene.c L16-20) */
struct obs_sceneitem *item = obs_scene_add(scene, image_src);

Configuring Position, Scale, and Rotation

struct vec2 pos = {1920.0f, 1080.0f};     /* absolute pixels */
struct vec2 scale = {0.5f, 0.5f};         /* 50% size */
float rot = 45.0f;                       /* degrees */

obs_sceneitem_set_pos(item, &pos);
obs_sceneitem_set_scale(item, &scale);
obs_sceneitem_set_rot(item, rot);
obs_sceneitem_set_alignment(item, OBS_ALIGN_CENTER);

Each setter marks the update_transform flag, triggering the pipeline on the next render frame.

Working with Group Scenes

/* Create a group (nested scene) */
struct obs_source *group_src = obs_source_create("scene", "Group", NULL, NULL);
struct obs_scene *group = obs_scene_from_source(group_src);

/* Add content to the group */
struct obs_sceneitem *sub_item = obs_scene_add(group, image_src);
obs_sceneitem_set_pos(sub_item, &(struct vec2){0, 0});

/* Add group to main scene */
struct obs_sceneitem *group_item = obs_scene_add(scene, group_src);
obs_sceneitem_set_scale(group_item, &(struct vec2){2.0f, 2.0f});

Accessing Transform Matrices

const struct matrix4 *m = obs_sceneitem_get_draw_transform(item);
printf("Scale X component: %f\n", m->x.x);

This matrix reflects the current state after update_item_transform processing.

Summary

  • Scene items are nodes in a thread-protected doubly-linked list, each wrapping a source with geometric and visibility state.
  • Transform updates occur lazily when properties change, calculating draw_transform and box_transform matrices via scale, rotation, translation, and alignment operations.
  • Bounds and cropping are applied before matrix construction, with separate handling for groups that maintain internal coordinate spaces.
  • Thread safety relies on recursive mutexes (video_mutex, audio_mutex) with strict ordering rules to prevent deadlocks during concurrent render and UI operations.
  • Blending and visibility affect both visual output and audio mixing through queued actions processed during the audio render path.

Frequently Asked Questions

What is the difference between draw_transform and box_transform in OBS Studio?

The draw_transform matrix (in obs_scene_item) is used for actual GPU rendering of the source, incorporating scale, rotation, and position. The box_transform matrix serves the UI selection box, providing hit-detection boundaries that account for the same geometric properties but without cropping or certain visual effects. Both are updated simultaneously in update_item_transform within libobs/obs-scene.c.

How does OBS Studio ensure thread safety during scene composition?

OBS uses two recursive mutexes per scene: video_mutex and audio_mutex. The render thread acquires video_mutex before iterating items in scene_video_render, while audio processing locks audio_mutex in scene_audio_render. A critical ordering rule prevents deadlocks: code must never lock the graphics mutex while holding a scene mutex, as documented in comments at lines 38–47 of obs-scene.c.

What triggers a transform update for an obs_scene_item?

The update_transform boolean flag triggers recalculation. Setters like obs_sceneitem_set_pos, obs_sceneitem_set_scale, or obs_sceneitem_set_rot mark this flag true. During the next render tick, update_transforms_and_prune_sources detects the flag (or canvas size changes) and invokes update_item_transform to rebuild the matrices before drawing.

How do group scenes handle transform inheritance?

Groups are specialized scenes (is_group = true) added as single items to parent scenes. When rendered, the parent scene's draw_transform becomes the base matrix for the group's internal items. The resize_group function automatically adjusts child positions and scales when the parent canvas changes, ensuring the group maintains its relative layout while scaling as a unified object.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →