|
3 | 3 | All notable changes to ShaderLab will be documented in this file. |
4 | 4 | Format follows [Keep a Changelog](https://keepachangelog.com/). |
5 | 5 |
|
6 | | -## [Unreleased] |
| 6 | +## [1.4.0] - 2026-05-05 |
| 7 | + |
| 8 | +### Removed |
| 9 | +- **Per-effect "follow the live monitor / working space" plumbing.** Removed all 5 host-side per-frame writer blocks from `MainWindow.xaml.cpp` (~150 LOC) and stripped every `_hidden` cbuffer field from 12 ShaderLab effects. The `Working Space` parameter node + property bindings (decision #51) are now the only path for tracking the active display profile from a graph. |
| 10 | + - **Effects converted** (12, all schema-broken — bumped `effectVersion`): Gamut Highlight, Luminance Highlight, CIE Chromaticity Plot, Gamut Source, Gamut Coverage, Gamut Map, ICtCp Gamut Map, ICtCp Boundary, ICtCp Tone Map (HDR → SDR), ICtCp Inverse Tone Map (SDR → HDR), ICtCp Highlight Desaturation, Luminance Statistics. Plus a light cleanup on the `Gamut Parameter` data node (dropped its `Working Space` value-enum entry). |
| 11 | + - **Replacement pattern**: each affected effect's gamut/range enum gains a `Custom` entry as the last index (existing static modes — `sRGB`, `DCI-P3`, `BT.2020`, etc. — are kept for strict-mode analysis without wiring 3 binds), and the host-managed primaries are promoted to first-class bindable `Float2` parameters (`RedPrimary`, `GreenPrimary`, `BluePrimary`, plus `WhitePoint` where applicable) gated by `visibleWhen "TargetGamut == 3"`. CIE Chromaticity Plot is the exception — its monitor primaries were always implicit, so the new `Float2` params are unconditional. Effects that consumed `(Ws)SdrWhiteNits_hidden` (ICtCp Tone Map / Inverse Tone Map / Highlight Desaturation / Luminance Statistics) drop the hidden field entirely; their existing numeric peak-nit parameter (`TargetPeakNits` / `SourcePeakNits` / `ClipNits`) becomes the single source. |
| 12 | + - **Old enum modes dropped**: every `Current Monitor` / `Working Space` entry from `TargetGamut`, `SourceGamut`, and `TargetRange`; entire `SdrWhiteSource` and `ClipSource` switch enums. |
| 13 | + - **Saved-graph breakage** (accepted, no other users yet): old graphs loaded from `.effectgraph` files keep stale `_hidden` properties in their `node.properties` map, but the shader cbuffer no longer references them and they're filtered out of the Properties panel + pin list (decision #35 retained for exactly this reason). Old `Working Space` enum-mode selections clamp to an invalid index → user re-picks. No graph format version bump. |
| 14 | + |
| 15 | +### Added |
| 16 | +- **HDR / gamut / tone-mapping MCP tool suite (10 new, 27 → 37 total).** Round 2 of the agent-driven HDR workflow: simulate any display profile, capture any node's output, drive the preview view, run GPU stats, sample pixel grids, and inspect HLSL — all without mutating UI state. |
| 17 | + - `list_display_profiles` — GET `/display/profiles`. Returns 7 built-in presets (sRGB SDR, sRGB 270, P3 600, P3 1000, BT.2020 1000, BT.2020 4000, AdobeRGB) + the active live/simulated profile + `isSimulated` flag. Full caps in JSON: hdr/peak/sdrWhite/min/maxFullFrame nits, primaries (CIE xy), gamut id. |
| 18 | + - `set_display_profile` — POST `/display/profile`. Apply a simulated profile via exactly one of `{preset, presetIndex, iccPath, custom}`. `custom` accepts the full `{name, hdrEnabled, sdrWhiteNits, peakNits, minNits, maxFullFrameNits, primaryRed, primaryGreen, primaryBlue, whitePoint, gamut}` schema; missing fields fall back to sane defaults. Drives `MainWindow::ApplyDisplayProfile`, marks the graph dirty, forces the next render frame. |
| 19 | + - `clear_simulated_profile` — POST `/display/profile/clear`. Reverts to the live OS-reported profile. |
| 20 | + - `render_capture_node` — POST `/render/capture-node` body `{nodeId, inline?}`. Captures any node's resolved image as PNG without touching `m_previewNodeId`. Forces a render frame first so dirty nodes evaluate. With `inline=true` returns MCP-native image content (`type:"image"`, `mimeType:"image/png"`); without it, writes a unique `%TEMP%\shaderlab_node_<pid>_<seq>.png` and returns the path. Disambiguates not-found (404) vs not-ready (409 + `notReady:true, reason:"dirty"|"missingInputs"`). |
| 21 | + - `preview_get_view` / `preview_set_view` / `preview_fit_view` — symmetric with the graph view tools but for the preview pane. zoom is clamped to [0.01, 100.0] (matches the wheel-zoom range, much wider than the graph view's [0.1, 5.0]); pan is unclamped. `preview_set_view` returns post-clamp values. |
| 22 | + - `image_stats` — POST `/render/image-stats` body `{nodeId, channels?, nonzeroOnly?}`. Per-channel min/max/mean/median/p95/sum + nonzero counts, GPU-reduced on a fresh FP32 render of the target. Defaults to luminance + R + G + B + A; pass `channels:["luminance"]` to skip the others. Backed by a new `GraphEvaluator::ComputeStandaloneStats(dc, image, channels, nonzeroOnly)` so MCP doesn't have to own a separate reduction instance or mutate the graph for diagnostics. |
| 23 | + - `read_pixel_region` — POST `/render/pixel-region` body `{nodeId, x, y, w, h}`. Reads a small w×h region of FP32 RGBA pixels (scRGB linear-light) row-major as a flat float array. Capped at 32×32 (1024 pixels) and per-axis at 64; oversize requests fail 400 with the cap quoted so agents can chunk. Out-of-bounds rects clip to the image; empty after clip → 404. |
| 24 | + - `effect_get_hlsl` — GET `/effect/hlsl/{nodeId}`. Reads a custom-effect node's HLSL source, parameter list, compile state, last runtime error. For ShaderLab library effects also returns `isLibraryEffect:true` + `shaderLabEffectId`/`shaderLabEffectVersion` so the agent knows what's read-only. Non-custom nodes return `hasCustomEffect:false` (200, not 404) so the agent can probe any node. |
| 25 | +- **Graph snapshot + view-control MCP tools** (4 new). AI agents can now request a PNG of the live node-graph editor view and steer its pan/zoom exactly the way a user would. Implementation lives in `MainWindow::CaptureGraphAsPng()`, `MainWindow::FitGraphView()`, and `NodeGraphController::ContentBounds()`. The shared `MainWindow::RenderGraphScene()` helper drives both the live render tick and the off-screen snapshot so the two never drift. |
| 26 | + - `graph_snapshot` — POST `/graph/snapshot`. Always writes a unique `%TEMP%\shaderlab_graph_snapshot_<pid>_<seq>.png`. With `inline=true`, the JSON-RPC response carries MCP-native `content[].type=image` + `mimeType=image/png` so the agent gets the bytes directly. Snapshot renders the current pan/zoom at the live swap-chain panel size — same dimensions the user sees. |
| 27 | + - `graph_get_view` — GET `/graph/view`. Returns `{zoom, panX, panY, viewportW, viewportH, contentBounds, zoomLimits}`. |
| 28 | + - `graph_set_view` — POST `/graph/view` body `{zoom?, panX?, panY?}` (any subset). Applies via `NodeGraphController::SetZoom`/`SetPanOffset`, which mark the canvas dirty so the next 16ms render tick shows the new view. zoom is clamped to [0.1, 5.0]; pan has no clamp. Coord convention: `screen = zoom * canvas + pan`. |
| 29 | + - `graph_fit_view` — POST `/graph/view/fit` body `{padding?}` (viewport DIPs, default 40). No-op on empty graph. |
| 30 | +- **`NodeGraphController::ContentBounds()`** — returns the AABB of all node visuals in canvas (pre-pan/zoom) space. `{0,0,0,0}` when no nodes are laid out. |
| 31 | +- **MCP activity indicator on the toolbar.** A single dot next to the MCP toggle pulses amber when the server has handled a request in the last few seconds and turns off when idle. Provides at-a-glance feedback that an external agent is actually connected and active. The amber colour was chosen because the original green was nearly invisible against the toggle's blue Checked background. |
| 32 | +- **`Working Space` parameter node + display-profile mirroring.** New ShaderLab Parameter effect (no input pins, no output image pin) that mirrors the active display profile from the top-bar profile selector — live OS-reported caps or any simulated preset / ICC the user has applied — into 14 typed analysis output fields: `ActiveColorMode` (0=SDR, 1=WCG/ACM, 2=HDR), `HdrSupported`, `HdrUserEnabled`, `WcgSupported`, `WcgUserEnabled`, `IsSimulated`, `SdrWhiteNits`, `PeakNits`, `MinNits`, `MaxFullFrameNits`, plus four CIE-xy primaries `RedPrimary` / `GreenPrimary` / `BluePrimary` / `WhitePoint` (each Float2). Bind any downstream property to these fields via the property-binding system — e.g. wire a tone-mapper's peak-nits to `working_space.PeakNits` and it tracks Display Settings or simulated profile changes automatically. Replaces ad-hoc per-effect "follow the working space" toggles. Updated in `MainWindow::UpdateWorkingSpaceNodes()` which runs on `ApplyDisplayProfile`, `RevertToLiveDisplay`, the display-change callback, and once per render tick (cheap node-list walk; only marks dirty when at least one field actually changed, so freshly-added nodes pick up live values immediately without hooking every AddNode site). |
| 33 | +- **`DisplayCapabilities` extended with ACM / WCG state** (`activeColorMode`, `hdrSupported`, `hdrUserEnabled`, `wcgSupported`, `wcgUserEnabled`). Sourced from a new `DisplayMonitor::QueryAdvancedColorInfo2()` helper that calls `DisplayConfigGetDeviceInfo` with `DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2` (type 15, requires SDK 10.0.26100). Falls back to deriving `activeColorMode` from `caps.hdrEnabled` when the type-15 query is unavailable. Also trusts DisplayConfig's `bitsPerColorChannel` over the legacy DXGI heuristic, which reports 8 in many WCG configurations even when the actual scanout is 10-bit. All seven preset factories now stamp coherent ACM/WCG flags via the new `StampSimulatedColorMode()` helper so simulated profiles report a self-consistent display mode through the Working Space node. |
| 34 | + |
| 35 | +### Changed |
| 36 | +- **The "Regular parameter nodes" branch in `GraphEvaluator::Evaluate` now unpacks the full `PropertyValue` variant** (`bool` → 0/1 float, `int32_t` / `uint32_t` → float, `float2` / `float3` / `float4` → component[0..N]) into `AnalysisFieldValue::components`. Previously only `float` was unpacked, so any vector or boolean property on a parameter node was silently zero. Used by the Working Space node for primary chromaticities (Float2) and capability flags (Float-as-bool); also benefits any future host-managed parameter nodes that need richer types. |
| 37 | +- **`Working Space Integration` README section rewritten** to describe the new "bind, don't hide" pattern for tracking the active display profile. Decision log gains entry #51; entry #35 (the `_hidden` suffix convention) cross-references #51 to record that the suffix filter is now legacy-compatibility only. |
| 38 | +- **`ICtCp Tone Map (HDR → SDR)` gained a `ToneLift` parameter** (default `0.0` = identity, range `[0..1]`). Adds an anchored polynomial mid-bump on top of the existing I-channel Reinhard compression: `f(x) = x + a·x·(1−x)` evaluated in normalized `[0, peakOut]` I-space then scaled back. Designed to let the operator approximate D2D `HdrToneMap`'s "make HDR readable on SDR" dark-end lift without giving up the hue-preservation property of pure I-channel compression. Quantitatively benchmarked against D2D HDR Tone Map on the `Colors of Journey` HDR test clip: at `ToneLift ≈ 0.6`, dark-region nit counts match D2D within ~10%; at `ToneLift = 0` the effect is its prior pure-compression behavior. Effect bumped from version 9 → 10 (drops `ToneLift` initialized to default on existing graphs). Note: the docstring no longer claims "saturation is preserved by construction" — only hue is. Lifting I without rescaling Ct/Cp can desaturate slightly, especially at higher `ToneLift`. |
| 39 | + |
| 40 | +### Fixed |
| 41 | +- **`ICtCpToScRGB` could emit NaN/Inf when `pqLms` exceeded the PQ valid range `[0, 1]`.** The function transformed ICtCp → PQ-encoded LMS via the inverse matrix and then called `PQ_EOTF` on each component without clamping. For modified ICtCp inputs (e.g. tone-mapped I) or out-of-gamut chroma, the matrix product could push an LMS component above 1 (where `PQ_EOTF`'s denominator goes through zero and then negative around `V ≈ 1.16`) or below 0 (where `pow` of a negative is undefined). Added `pqLms = saturate(pqLms);` before the EOTF calls. In-domain values are unaffected; out-of-domain values now saturate to the nearest representable nit value rather than producing NaN that propagates into the inverse XYZ→scRGB matrix. Benefits all 8 ICtCp-using effects. |
| 42 | +- **`DispatchSync` was UB on timeout.** The original wait-and-deref pattern closed the event after a 10-second timeout, then dereferenced `*result` without checking `WAIT_OBJECT_0` — and the in-flight lambda still held references to stack `result`/`ex`/`event` which dangled if the caller had returned. Replaced with a `shared_ptr<State>` capture so the lambda can safely complete after the caller times out, an explicit `WAIT_OBJECT_0` check that throws on timeout (caller's `catch` returns 500 cleanly), and a 30s timeout for the heavier compute/readback tools. Closed by the new round-2 routes that need it; old routes benefit too. |
| 43 | +- **Snapshot capture no longer suppresses the live editor repaint.** `NodeGraphController::Render()` clears `m_needsRedraw` at the end. `CaptureGraphAsPng()` now snapshots the dirty flag before its render and re-sets it after, so an MCP-driven snapshot taken between frames doesn't cancel the next live render. |
7 | 44 |
|
8 | 45 | ## [1.3.9] - 2026-05-05 |
9 | 46 |
|
|
0 commit comments