Skip to content

Commit ef13322

Browse files
committed
v1.3.8: ICtCp tone-mapping suite + MCP chunked transport
Added - ICtCp Tone Map (HDR -> SDR), ICtCp Inverse Tone Map (SDR -> HDR), ICtCp Round-Trip Validator, ICtCp Saturation, ICtCp Highlight Desat - OS-reported SDR white level (DisplayConfigGetDeviceInfo), live in status bar; WsSdrWhiteNits_hidden injected per-frame - Add-Node flyout sub-grouping (Analysis split into Highlights / Scopes / Statistics / Tone Mapping / Gamut Mapping / Comparison) Changed - Perceptual Gamut Map renamed to ICtCp Gamut Map (alias kept) Fixed - MCP server now decodes Transfer-Encoding: chunked (was the cause of the VS 18 MCP-client connect crash via JsonObject::Parse). Adds non-throwing TryParse, 202 Accepted for notifications/*, GET / health-check route. - All five ICtCp pixel shaders sampled the same edge texel: D2D TEXCOORD is pixel-space, not normalized. Switched to Source.Load. - ReinhardCompressI / ReinhardExpandI replaced with the anchored Mobius form; peaks now map to peaks exactly. Argument-order in the Inverse Tone Map call site fixed; helper docs match algebra. - Over-range inputs to the helpers clamped before mapping, so they saturate to the target peak rather than racing to the asymptote.
1 parent 35fffac commit ef13322

15 files changed

Lines changed: 1053 additions & 88 deletions

.mcp.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"inputs": [],
3+
"servers": {
4+
"ShaderLab": {
5+
"type": "http",
6+
"url": "http://localhost:47808/",
7+
"headers": {}
8+
}
9+
}
10+
}

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@
33
All notable changes to ShaderLab will be documented in this file.
44
Format follows [Keep a Changelog](https://keepachangelog.com/).
55

6+
## [Unreleased]
7+
8+
## [1.3.8] - 2026-05-05
9+
10+
### Added
11+
- **ICtCp tone-mapping suite (initial)**: three new effects under *Analysis → Tone Mapping*. `ICtCp Tone Map (HDR → SDR)` and `ICtCp Inverse Tone Map (SDR → HDR)` apply Reinhard compression / expansion to I in BT.2100 ICtCp space, leaving Ct/Cp untouched so hue and saturation are preserved by construction. `ICtCp Round-Trip Validator` is a diagnostic effect that outputs `|scRGB→ICtCp→scRGB - in| × Gain` — used to verify the conversion math is correct (renders black on a correct image). All three opt into `SdrWhiteNits_hidden` so future auto-bind affordances can drive Target/SourcePeakNits from the live OS SDR-white value.
12+
- **Color-math HLSL helpers**: `NitsToI`, `IToNits`, `ReinhardCompressI`, `ReinhardExpandI` for I-channel tone mapping in PQ-encoded nit space. Live next to the existing `ScRGBToICtCp` / `ICtCpToScRGB` pair in `GetColorMathHLSL()`.
13+
- **OS-reported SDR white level**: `DisplayMonitor` now queries `DisplayConfigGetDeviceInfo(DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL)` so `sdrWhiteLevelNits` tracks the Windows *Settings → Display → HDR → "SDR content brightness"* slider in real time. Falls back to 80 nits when the call isn't available (older Windows builds, virtual outputs). Status bar now displays it next to the peak nit value (e.g. `Max Luminance: 1000 nits (SDR white 240)`). Simulated `DisplayProfile` presets continue to override the live value through the existing path.
14+
- **`SdrWhiteNits_hidden` / `WsSdrWhiteNits_hidden`** hidden-default keys for effects that need scRGB-1.0-in-nits. Injected per frame from `RenderFrame` with the same change-detected pattern as `MonMaxNits_hidden` and `WsRedX_hidden`.
15+
- **Add Node flyout sub-grouping**: `ShaderLabEffectDescriptor` gained a `subcategory` field. The Analysis category — which had grown to 16+ entries — is now folded into **Comparison**, **Gamut Mapping**, **Highlights**, **Scopes**, **Statistics**, and **Tone Mapping** sub-trees. Effects without a subcategory remain at the top level under their category.
16+
17+
### Changed
18+
- **`Perceptual Gamut Map` renamed to `ICtCp Gamut Map`** (effectId + display name) for consistency with the existing `ICtCp Boundary` effect and the new ICtCp tone-mapping suite. Saved graphs that reference the old ID resolve through a legacy alias in `FindById`, so existing `.effectgraph` files still load — they'll surface the standard "Update Effect" prompt on next save. Effect version bumped 7 → 8.
19+
20+
### Fixed
21+
- **MCP server now accepts `Transfer-Encoding: chunked` requests.** Visual Studio's MCP client uses chunked HTTP rather than `Content-Length`; our raw-Winsock reader was passing the chunk framing (`<hex-size>\r\n<payload>\r\n0\r\n\r\n`) straight to `JsonObject::Parse`, which threw `0x83750007 "Invalid JSON string"` on every connect. The reader now detects chunked encoding, waits for the terminating zero-length chunk, and decodes the framing into the actual JSON payload. Content-Length requests are unchanged. Same path also adds: a non-throwing `JsonObject::TryParse` wrapper that returns a JSON-RPC `-32700 Parse error` response instead of bubbling a `winrt::hresult_error`; correct `202 Accepted` (not `200 OK`) for `notifications/*` per the MCP Streamable HTTP spec; a small `GET /` health-check route so clients that probe before posting don't get a 404; and proper status-text for 202/204/400/500 in the HTTP response builder.
22+
- **All five ICtCp pixel shaders were sampling the same edge texel for every output pixel.** They were calling `Source.Sample(Sampler, uv)` with `uv` from `TEXCOORD`, but D2D `TEXCOORD` is in pixel/scene space, not normalized [0,1]. Every output pixel was reading from texel ~256 with edge clamping, so the entire image came out as one constant color regardless of input. Switched to `Source.Load(int3(uv, 0))` to match the Gamut Highlight / scope pattern that was already correct. Affects: `ICtCp Round-Trip Validator`, `ICtCp Tone Map (HDR → SDR)`, `ICtCp Inverse Tone Map (SDR → HDR)`, `ICtCp Saturation`, `ICtCp Highlight Desaturation`. Effect versions bumped to invalidate cached bytecode.
23+
- **`ReinhardCompressI` / `ReinhardExpandI` were unanchored.** The original form `peakOut * t / (1 + t*(1 - ratio))` had `f(peakIn) = peakOut / (2 - ratio)` instead of `peakOut`, so peaks did not map to peaks: HDR→SDR compression undershot the SDR target, SDR→HDR expansion overshot the HDR target by hundreds of percent (a 1000-nit SDR-white input produced ~14,943 nits at TargetPeakNits=1000). Replaced with the anchored Möbius `f(I) = I·peakIn·peakOut / (peakIn·peakOut + I·(peakIn − peakOut))` and its analytic inverse — same shape near zero (slope 1, "Reinhard" feel in the dark end), but `f(0) = 0`, `f(peakIn) = peakOut` exactly, with a smooth shoulder. Round-trip error stays at FP16 noise (~7e-10 per channel at Gain=100k).
24+
- **`ReinhardExpandI` had its peakIn/peakOut convention inverted in the call site.** The doc-comment said `peakIn = SDR, peakOut = HDR`, but the algebra is the literal inverse of `ReinhardCompressI(peakIn=HDR, peakOut=SDR)`, so the call inside the Inverse Tone Map shader was running the curve in the wrong direction (input got dimmer with higher Strength). Both the doc-comment and the call site now agree: peakIn is the *uncompressed* (HDR) range, peakOut is the *compressed* (SDR) range, for both Compress and Expand.
25+
- **Over-range inputs to the ICtCp tone-mapping helpers walked toward the Möbius asymptote.** A 10,000-nit input through an Inverse Tone Map configured for SourcePeakNits=100 produced 1.4 million nits because the helper extrapolated past its anchor onto the rising branch beyond peakIn. Both `ReinhardCompressI` and `ReinhardExpandI` now clamp their input to the valid input range before mapping, so out-of-domain inputs saturate cleanly to the target peak instead of overshooting.
26+
627
## [1.3.7] - 2026-05-05
728

829
### Changed

0 commit comments

Comments
 (0)