Skip to content

Commit 348280c

Browse files
committed
fix(fusion): a value written under a comp lock never reaches the render
Every Fusion parameter this server wrote was stored in the graph, returned by GetInput, returned by our own get_input — and ignored at render. The cause is the Comp.Lock()/Unlock() pair the value writes were wrapped in. Measured live on Studio 19.1.3.7 with MediaIn -> Blur(XBlurSize 20) -> MediaOut on a media-backed clip, rendering the same 48 frames to H.264 each time: fusion_comp set_input, write inside Comp.Lock() PSNR inf IGNORED the same write with the lock removed PSNR 24.38dB RENDERED raw tool.XBlurSize = 20.0 PSNR 24.38dB RENDERED raw tool.SetInput("XBlurSize", 20) PSNR 24.38dB RENDERED Under the lock the delivered file is bit-identical to the no-comp baseline; without it the file shrinks 2.0 MB -> 727 KB, as a blur should. The variable was isolated against the comp handle (AddFusionComp, GetFusionCompByIndex and GetFusionCompByName all render), the node name, and the write form. Only the lock around the write decides it. Structural edits are NOT affected: AddTool and ConnectInput inside a lock render normally and keep their locks. This is not "Lock is unsafe" — the lock suppresses the parameter-change invalidation that a value write depends on. Six sites held a lock across a value write: fusion_comp set_input, safe_set_inputs, bulk_set_inputs, bulk_set_expressions, the Text+ writer behind set_text, and add_fusion_mask. The last was the worst of them — the lock spanned AddTool *and* every input write, so a mask came out at default size and position with every parameter the caller passed silently doing nothing. There the lock now closes once the node is created and renamed, and the input writes follow outside it. This is the failure mode our own guidance describes — prove a Fusion or grade claim with a rendered frame, never with readback — except the cause was ours, not Resolve's. It also explains an unknown share of past "the comp was ignored" reports, which look identical from the API side. api_truth gains a Composition.Lock entry, and the AddFusionComp entry records that its 2026-08-02 rooted-comp result reproduced here at PSNR 24.38 dB. Tests, both mutation-checked by reintroducing the lock in set_input: tests/live_fusion_value_write_validation.py renders a baseline, writes through the compound tool, renders again, and asserts PSNR actually moved (it reports "PSNR inf -> IGNORED at render" against the pre-fix code). tests/test_fusion_value_write_lock.py is an AST guard over src/ that fails any value write inside a lock region; it found the add_fusion_mask site that a manual read had missed. Suite: 2786 passed, 1 skipped. add_fusion_mask re-verified live end to end (Width/Height/Center all land on the real graph).
1 parent 22cfafb commit 348280c

13 files changed

Lines changed: 700 additions & 145 deletions

CHANGELOG.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,67 @@
22

33
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
44

5+
## What's New in v2.98.5
6+
7+
**Every Fusion parameter this server wrote was ignored at render.** A value
8+
write (`SetInput` / `SetExpression`) wrapped in `Comp.Lock()`/`Unlock()` is
9+
stored in the graph and reads back correctly — `GetInput` returns it, and so
10+
did this server's own `get_input` — while the delivered render ignores it
11+
completely. Found on 2026-08-21 while re-running a Fusion isolation on Studio
12+
19.1.3.7 to settle a conflicting measurement reported in
13+
[#156](https://github.com/samuelgursky/davinci-resolve-mcp/pull/156).
14+
15+
Measured on Studio 19.1.3.7 with `MediaIn -> Blur(XBlurSize 20) -> MediaOut` on
16+
a media-backed clip, rendering the same 48 frames to H.264 each time:
17+
18+
| value written via | render vs no-comp baseline |
19+
| --- | --- |
20+
| `fusion_comp set_input` (write inside `Comp.Lock()`) | PSNR **inf** — bit-identical, ignored |
21+
| the same write, lock removed | PSNR **24.38 dB**, 2.0 MB → 727 KB |
22+
| raw `tool.XBlurSize = 20.0` | PSNR **24.38 dB** |
23+
| raw `tool.SetInput("XBlurSize", 20)` | PSNR **24.38 dB** |
24+
25+
The variable was isolated against the comp handle (`AddFusionComp`,
26+
`GetFusionCompByIndex` and `GetFusionCompByName` all render), the node name, and
27+
the write form. Only the lock around the write decides it. **Structural** edits
28+
are unaffected — `AddTool` and `ConnectInput` inside a lock render normally — so
29+
this is not "Lock is unsafe"; the lock suppresses the parameter-change
30+
invalidation that a value write depends on.
31+
32+
### Fixed
33+
34+
- **Six value-write sites no longer hold a comp lock across the write:**
35+
`fusion_comp set_input`, `fusion_comp safe_set_inputs`, `bulk_set_inputs`,
36+
`bulk_set_expressions`, the Text+ writer behind `set_text`, and
37+
`add_mask` — where the lock spanned `AddTool` *and* every input write, so a
38+
mask was created at default size and position and every parameter the caller
39+
passed did nothing. Structural work keeps its lock; in `add_mask` the lock now
40+
closes after the node is created and renamed.
41+
42+
### Why this went unnoticed
43+
44+
Every readback the API offers agreed with the value that was written. This is
45+
the failure mode the repo's own guidance describes — prove a Fusion or grade
46+
claim with a rendered frame, never with readback — except the cause was ours,
47+
not Resolve's. It also explains an unknown share of past "the comp was ignored"
48+
reports, which look identical from the API side.
49+
50+
### Tests
51+
52+
- `tests/live_fusion_value_write_validation.py` — renders a baseline, writes a
53+
blur size through the compound tool, renders again, and asserts PSNR actually
54+
moved. Disposable project, synthetic media, restores the previous project.
55+
- `tests/test_fusion_value_write_lock.py` — AST guard failing any value write
56+
that sits inside a `Comp.Lock()`/`Unlock()` region, with a self-check that the
57+
guard can still see a known-bad shape.
58+
59+
Both were mutation-checked against the pre-fix code: reintroducing the lock in
60+
`set_input` makes the live harness report `PSNR inf -> IGNORED at render` and
61+
fails the offline guard.
62+
63+
- `api_truth`: new `Composition.Lock` entry; the `AddFusionComp` entry records
64+
that its 2026-08-02 rooted-comp result **reproduced** on 19.1.3.7 (PSNR 24.38 dB).
65+
566
## What's New in v2.98.4
667

768
**Setup reported success over an install that could never work.** Reported and

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
English | [简体中文](README.zh-CN.md)
44

5-
[![Version](https://img.shields.io/badge/version-2.98.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5+
[![Version](https://img.shields.io/badge/version-2.98.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
66
[![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
77
[![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
88
[![Tools](https://img.shields.io/badge/MCP%20Tools-35%20(353%20full)-blue.svg)](#server-modes)

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[English](README.md) | 简体中文
44

5-
[![Version](https://img.shields.io/badge/version-2.98.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5+
[![Version](https://img.shields.io/badge/version-2.98.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
66
[![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
77
[![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
88
[![Tools](https://img.shields.io/badge/MCP%20Tools-35%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
1212
[![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
1313
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
1414

15-
> 本翻译对应 v2.98.4 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15+
> 本翻译对应 v2.98.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
1616
1717
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
1818

docs/SKILL.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,6 +1690,17 @@ Target a comp either from a timeline item (pass `clip_id`, `timeline_item_id`, o
16901690
`timeline_item={track_type, track_index, item_index}`) or from the active Fusion
16911691
page comp (omit timeline scope).
16921692

1693+
READBACK IS NOT PROOF FOR FUSION PARAMETERS. Up to v2.98.4 every value write
1694+
here ran inside a `Comp.Lock()`, and a value written under a comp lock is stored
1695+
in the graph and returned by `get_input` while the RENDER ignores it entirely
1696+
(Studio 19.1.3.7: PSNR inf vs the no-comp baseline — the delivered file was
1697+
bit-identical to no comp at all). Fixed in v2.98.5, and guarded by
1698+
`tests/test_fusion_value_write_lock.py` plus the rendered-frame harness
1699+
`tests/live_fusion_value_write_validation.py`. The lesson outlives the bug: a
1700+
Fusion parameter that reads back correctly has proven nothing about the output,
1701+
so confirm any Fusion look with a rendered frame (`gallery_stills
1702+
grab_and_export` or a frame from a delivered render), never with `get_input`.
1703+
16931704
Key actions:
16941705
- `add_tool(tool_type, x?, y?, name?)` — common types: `Merge`, `Background`,
16951706
`TextPlus`, `Transform`, `Blur`, `ColorCorrector`, `RectangleMask`,

docs/reference/api-limitations.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ that none exists).
1212

1313
**Verified on:** DaVinci Resolve Studio 21.0.2
1414

15-
**Totals:** 27 missing capabilities, 35 bugs / unreliable behaviors.
15+
**Totals:** 27 missing capabilities, 36 bugs / unreliable behaviors.
1616

1717
The authoritative source is the runtime-queryable `api_truth` ledger
1818
(`resolve_control api_truth "<query>"`); this document is generated from
@@ -339,10 +339,18 @@ values, or automation-hostile modal prompts.
339339
- **Workaround / current handling:** Author OTIO for Resolve by mirroring what Resolve itself exports, and give every event its media timecode origin. editorial.convert_to_interchange (target 'otio') does this and reports any event whose origin had to be assumed in `mediaOriginAssumed` — a non-empty list means the file will only import if that media really starts at 00:00:00:00. To debug a refusal, export any timeline with EXPORT_OTIO and diff your document against it; do NOT chase missing media or reach for sanitize_media, which cannot even parse a .otio (it is JSON, not XML).
340340
- **Tags:** timeline, import, interchange, otio, silent-failure, conform
341341

342+
### Composition.Lock (suppresses render invalidation for value writes)
343+
344+
- **Object:** `Composition (Fusion, via TimelineItem comps)`
345+
- **Signature:** `Lock() / Unlock()`
346+
- **Behavior:** A value write performed between Comp.Lock() and Comp.Unlock() — SetInput(), or Input.SetExpression() — is stored in the graph and reads back correctly from GetInput(), but is NOT applied when the timeline is rendered. Measured live on Studio 19.1.3.7 (2026-08-21) with MediaIn -> Blur(XBlurSize 20) -> MediaOut on a media-backed clip: written under the lock the delivered H.264 render is bit-identical to the no-comp baseline (ffmpeg PSNR inf); the identical write with the lock removed renders at PSNR 24.38 dB and the file shrinks 2.0 MB -> 727 KB, as a blur should. The variable was isolated against the comp handle (AddFusionComp, GetFusionCompByIndex and GetFusionCompByName all render), the node name, and the write form (attribute assignment and SetInput both render unlocked). STRUCTURAL edits are unaffected: AddTool and ConnectInput inside a lock render normally, so the lock is not broadly unsafe — it suppresses the parameter-change invalidation that a value write depends on. Lock() is widely recommended for batching Fusion edits, which is how this reaches production code.
347+
- **Workaround / current handling:** Never hold a comp lock across a value write. Lock only structural work (AddTool/ConnectInput) and set inputs outside it. Because every readback the API offers agrees with the value that was written, this failure is invisible without a render — prove Fusion parameter changes with a delivered frame or gallery_stills grab_and_export, never with GetInput.
348+
- **Tags:** fusion, silent-failure, render, readback
349+
342350
### TimelineItem.AddFusionComp / LoadFusionCompByName
343351

344352
- **Object:** `TimelineItem (media-backed clip)`
345-
- **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally.
353+
- **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally. REPRODUCED 2026-08-21 on Studio 19.1.3.7: a rooted MediaIn -> Blur -> MediaOut comp built entirely through the API renders (PSNR 24.38 dB vs the no-comp baseline), so the 2026-08-02 correction stands. Note that an important share of 'the comp was ignored' readings are NOT this entry at all but the Composition.Lock bug above — a parameter written under a comp lock reads back correctly and never reaches the render, which looks identical from the API side.
346354
- **Workaround / current handling:** Wire the graph so MediaOut descends from MediaIn — that is the difference between a comp that renders and one that is silently bypassed, and it is what made this look like 'Fusion never renders from the API'. Never leave a tool unrooted: a MediaOut fed by a tool with no source failed the render job outright. For text or effects over picture, insert a Fusion title/generator as its own timeline clip and set its Text+ (fusion_comp set_text_plus), rather than attaching a comp to the media clip. Note the destination track cannot be chosen from the API (see the Track Selector entry), so overlaying onto an existing clip's track is not currently reachable end-to-end. Building the comp in the Fusion page UI works; only the API-created comp is ignored.
347355
- **Tags:** fusion, silent-failure, render
348356

install.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
# ─── Version ──────────────────────────────────────────────────────────────────
3939

40-
VERSION = "2.98.4"
40+
VERSION = "2.98.5"
4141
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
4242
# Resolve's scripting bridge loads into newer interpreters on recent builds
4343
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "davinci-resolve-mcp",
3-
"version": "2.98.4",
3+
"version": "2.98.5",
44
"description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
55
"license": "MIT",
66
"author": "Samuel Gursky <samgursky@gmail.com>",

src/granular/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
handlers=[logging.StreamHandler()],
8888
)
8989

90-
VERSION = "2.98.4"
90+
VERSION = "2.98.5"
9191
logger = logging.getLogger("davinci-resolve-mcp")
9292
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
9393
logger.info(f"Detected platform: {get_platform()}")

0 commit comments

Comments
 (0)