Skip to content

Skip obs_set_video_info when a canvas' requested settings are unchanged - #1788

Merged
summeroff merged 3 commits into
stagingfrom
perf/skip-unchanged-video-context
Sep 30, 2026
Merged

summeroff merged 3 commits into
stagingfrom
perf/skip-unchanged-video-context

Conversation

@au-sf

@au-sf au-sf commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Summary

osn::Video::SetVideoContext passes every request straight to obs_set_video_info, and libobs does not short-circuit: each call stops the graphics thread, frees the canvas' video and re-initialises it ([VIDEO_CANVAS] wait obs_graphics_thread to stop, obs_free_video), about 130 ms per call on an M4 Max, whether or not a single value changed. Streamlabs Desktop re-applies unchanged settings more than it means to: at startup it pushed each display's settings twice (fixed on the app side in streamlabs/desktop#6235), and updateObsSettings / refreshVideoSettings push the current values on every settings write. This change makes an identical re-apply a no-op inside OSN, so every caller benefits and the app-side fix becomes belt and braces.

What it does

Server (osn-video.cpp). SetVideoContext builds the full obs_video_info exactly as before, including the output_format / colorspace / range it reads from basic.ini, and then compares it with the settings last applied successfully to that canvas. If every field matches (graphics_module, fps num/den/type, base and output size, output format, colorspace, range, scale type, adapter, gpu_conversion), it logs [VIDEO_CANVAS] Set video context skipped for 0x…: settings unchanged, refreshes the SDR/HDR video levels from config as the success path does, and returns Ok without calling obs_set_video_info, autoOptimizer::CancelActiveSession() or stopConnectingStreamingOutputs().

The "last applied" record is OSN's own (static std::unordered_map<const obs_video_info *, obs_video_info> behind a mutex), written only after obs_set_video_info returns OBS_VIDEO_SUCCESS and erased in RemoveVideoContext on both paths that free the canvas id. Nothing is read from libobs' initialized flag, so a freshly created canvas always gets its first real apply even if the request happens to equal the defaults, a canvas whose apply failed retries next time, and a change made through basic.ini (colour format, colour space, range) still shows up as a difference because those fields are part of the comparison. The levels refresh on the skip path keeps SdrWhiteLevel / HdrNominalPeakLevel edits taking effect exactly when they did before.

Test (tests/osn-tests/src/test_osn_video.ts). "Re-applying identical video settings is accepted and later changes still apply": sets a context, re-applies the same object and an equal copy, reads back unchanged, then applies a real change and reads that back. In the server log the two re-applies produce the skipped line and the third call goes through.

Files changed: obs-studio-server/source/osn-video.cpp, tests/osn-tests/src/test_osn_video.ts

What I checked for anything relying on the old behaviour

  • OSN's own libobs video calls outside this handler are untouched and separate: OBS_API_initAPI's initial obs_reset_video, OBS_service::doResetVideoContext (nodeobs_service.cpp, which calls obs_set_video_info on base_canvas directly) and saveVideoSettings' obs_reset_video.
  • Device-loss handling (nodeobs_common.cpp): OnDeviceLost is a no-op and OnDeviceRebuilt only resizes displays; neither re-applies video settings to force a reset.
  • Every Desktop push into a context goes through the video setter, i.e. this handler: migrateSettings, loadLegacySettings, establishVideoContext, updateObsSettings, the vertical sync in settings-v2/video.ts. None is a deliberate "re-apply to reset"; Desktop's only recovery path (videoContextError) destroys and recreates the canvas, which is a fresh canvas and therefore always applies.
  • SetLegacySettings only writes basic.ini and is unaffected.

Performance Implications

Net reduction: an identical re-apply now costs a field comparison instead of a ~130 ms pipeline rebuild. Nothing changes for a request that differs in any field. Memory is one obs_video_info per live canvas.

Verification

  • Built on macOS arm64 (Unix Makefiles, RelWithDebInfo, Electron 29.3.1 headers) from staging (71c4c03a).
  • osn-video suite under electron-mocha: 6 passing, including the new test; the server log for the run shows exactly two Set video context skipped lines.
  • Exercised from Streamlabs Desktop (macOS arm64), this build dropped into the app's node_modules/obs-studio-node and the app built from plain master (which still double-pushes at startup): the OBS server log shows, per canvas, the first Set video context applied and the second skipped … settings unchanged within 1 ms, for both the horizontal and vertical canvas; the app initialised normally (2.6 s cold) and the settings OBS reported matched the app's state. With the app built from Apply a Display's Video Settings to Its OBS Context Once When Establishing It desktop#6235 instead, no skip fires, as expected.
  • clang-format (repo style) on the touched file changed only the new code.
  • Windows is not built locally; CI's Windows job is the Windows verification. The comparison and bookkeeping are platform-neutral; graphics_module is compared as a string so the D3D11/OpenGL names both work.

Notes

Two other things the same server logs showed, not addressed here: on a restart after an unclean exit, mac-avcapture-legacy's module load blocks for ~5 s on macOS (Desktop stopped creating av_capture_input sources in 2025-10 and migrates old ones, so obs_add_disabled_module("mac-avcapture-legacy") before obs_load_all_modules2 looks possible, pending a product check); and OBS_settings_getVideoDevices on macOS still enumerates through a throwaway macos_avcapture source (~140 ms at startup) where the audio lists moved to getDevices() in #1691.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Failed applies can leave stale cached settings, and the test does not verify that reinitialization was skipped.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Optimizes repeated video-context updates by skipping unchanged libobs reinitialization.

Changes:

  • Caches successfully applied settings per canvas.
  • Refreshes video levels on skipped updates.
  • Adds repeated-update coverage.
File Description
obs-studio-server/​source/​osn-video.cpp Adds video-setting comparison and caching.
tests/​osn-tests/​src/​test_osn_video.ts Tests repeated settings assignments.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread obs-studio-server/source/osn-video.cpp
Comment thread tests/osn-tests/src/test_osn_video.ts
libobs frees video for every canvas on a failed set/remove/reset, so a per-canvas record could skip the re-apply needed to recover. Tests now assert the skip path via the server log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@summeroff

Copy link
Copy Markdown
Contributor

Review follow-up (pushed in c2ad4d3):

  • libobs video state is process-wide. A failed obs_set_video_info, obs_remove_video_info, or obs_reset_video frees video for every canvas, but the per-canvas "last applied" record survived. Re-applying the last good settings was then skipped and returned Ok, so video never came back.
  • Fix: any failed set/remove, and every Advanced-settings reset, now clears the records for all canvases.
  • Tests: the identical re-apply test now asserts that the skip happens (via the server log), and a new test covers re-applying after a failed update. Neither has run locally; CI is their first run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

A failed direct video reset can leave stale cache entries that incorrectly suppress canvas recovery.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment on lines +353 to +358
// libobs video state is process-wide, so a failed call can leave every canvas torn down.
void osn::Video::InvalidateAppliedVideoInfo()
{
std::lock_guard<std::mutex> lock(lastAppliedVideoMutex);
lastAppliedVideo.clear();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6081996: doResetVideoContext now invalidates the applied video info after obs_set_video_info (success or failure) and in the catch path.

doResetVideoContext calls obs_set_video_info directly, so a failed reset left the skip-unchanged cache stale and a later re-apply of the old settings could be skipped while video was torn down.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@summeroff
summeroff merged commit 3fde7d7 into staging Sep 30, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants