Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
138 commits
Select commit Hold shift + click to select a range
95766e7
docs(specs): specify the GPU pass fusion pipeline
yuto-trd Aug 12, 2026
35e7f28
refactor(engine)!: record then plan the render pipeline and fuse GPU …
yuto-trd Aug 12, 2026
ad27305
test(engine): cover the fused render pipeline
yuto-trd Aug 12, 2026
f9c16ee
fix(review): give drawable-brush thumbnails a target domain
yuto-trd Aug 12, 2026
78b2772
fix(engine): restore drawable-brush stretch against intrinsic bounds
yuto-trd Aug 12, 2026
5c8a7e6
perf(engine): crop drawable-brush content without a device snapshot
yuto-trd Aug 12, 2026
acdf35b
fix(engine): count deferred operations in FilterEffectContext.CountItems
yuto-trd Aug 12, 2026
5625a65
fix(editor): report engine-resource failures instead of killing the r…
yuto-trd Aug 12, 2026
699332c
feat(engine)!: expose drawable-brush materialization and the cache op…
yuto-trd Aug 12, 2026
a7ade16
refactor(engine): name the filter-effect fallback path for what it is
yuto-trd Aug 12, 2026
88d311c
fix(editor): close the failure paths the review fixes opened
yuto-trd Aug 12, 2026
8be2d76
docs(ai-workflow): point effect authors at the recorded surfaces
yuto-trd Aug 12, 2026
e296e21
fix(benchmarks): read pipeline counters from the surviving statistics
yuto-trd Aug 12, 2026
6a09384
test(benchmarks): cover spatial-effect chain depth
yuto-trd Aug 12, 2026
b37927c
perf(engine): reduce filter-effect GPU synchronization
yuto-trd Aug 13, 2026
15cd411
test(engine): drop the orphaned GetMaximumDimension test fakes
yuto-trd Aug 13, 2026
20a9dd0
fix(editor): bound export GPU memory with periodic retention checkpoints
yuto-trd Aug 13, 2026
999ad72
fix(engine)!: fail delivery renders on custom-effect target allocatio…
yuto-trd Aug 13, 2026
20aa0ce
fix(engine): honor the destination clip and preview degradation when …
yuto-trd Aug 13, 2026
ebce996
fix(nodegraph): decide preview emptiness from the resolved output extent
yuto-trd Aug 13, 2026
e4f0f99
fix(editor): rasterize the selected drawable over its output bounds
yuto-trd Aug 13, 2026
d8be641
docs(specs): restore the migration contract and correct the task state
yuto-trd Aug 13, 2026
e3927dd
fix(engine): bound the filter layer to the content being filtered
yuto-trd Aug 13, 2026
991f49e
perf(engine)!: widen GPU pass fusion beyond current-pixel colour shaders
yuto-trd Aug 15, 2026
1bc6920
perf(engine): cut fusion, synchronization and readback overhead
yuto-trd Aug 15, 2026
58d8e0d
fix(engine): synchronize custom-effect sources across the Vulkan boun…
yuto-trd Aug 15, 2026
cfaaad6
test(engine): bound the colour-filter parity oracle to the reference'…
yuto-trd Aug 15, 2026
3dde97c
fix(engine): repair the rendering regressions found by differential a…
Aug 16, 2026
ee50706
fix(engine)!: apply a group's filter per child and eliminate non-fini…
Aug 17, 2026
f1bb791
fix(engine): submit the target clear before a native writer fills it
Aug 18, 2026
bab9370
fix(engine): round a whole-source shader's bounds on the shared devic…
Aug 18, 2026
1db58bc
test(engine): retarget the selected-drawable group test to the group'…
yuto-trd Aug 18, 2026
ebf1bc5
fix(engine): round a callback's own bound out to whole device pixels
yuto-trd Aug 18, 2026
b17fab1
fix(engine): draw a particle's source instead of tiling it as a shader
yuto-trd Aug 18, 2026
7327911
WIP: tasks 1-5 vulkan effect-layer native work
yuto-trd Aug 17, 2026
d6774c6
WIP: rebase integration fixes
yuto-trd Aug 18, 2026
9352a5c
Merge branch 'main' into speckit/004-gpu-pass-fusion-unified
yuto-trd Aug 19, 2026
5d918b9
style(tests): restore the UTF-8 BOM on the native scratch-texture tests
yuto-trd Aug 19, 2026
283cdcc
fix(engine): keep a render pass from swallowing the transfers recorde…
yuto-trd Aug 19, 2026
2974a60
fix(engine)!: stop recording an identity colour matrix as a shader stage
yuto-trd Aug 19, 2026
e90a4d7
test(engine): make the GPU oracles hold on a backend with real fp16
yuto-trd Aug 19, 2026
a9a999a
fix(engine): stop the colour keys from hinging on bit-exact colour eq…
yuto-trd Aug 19, 2026
5d2b603
fix(engine): stop an inset stroke from cropping the fill it surrounds
yuto-trd Aug 19, 2026
6dfd0f2
fix(engine)!: evaluate a whole-source shader over its complete output
yuto-trd Aug 19, 2026
674c82a
fix(engine): bound a perspective quad that crosses the camera plane
yuto-trd Aug 19, 2026
66cd2dc
fix(engine)!: stop a negative morphology radius from cropping the con…
yuto-trd Aug 19, 2026
554bd62
fix(engine): anchor the Skia chain frame to the bounds each item maps
yuto-trd Aug 19, 2026
680ee2c
fix(engine): give the target-rewriting effects a declared output extent
yuto-trd Aug 19, 2026
7e2d928
fix(engine)!: rasterize a filtered hairline on the grid it is composi…
yuto-trd Aug 19, 2026
715d3a5
fix(engine): document what the perspective near plane really bounds
yuto-trd Aug 19, 2026
72014b7
test(engine): pin the Skia chain anchor on a non-origin-anchored input
yuto-trd Aug 19, 2026
f082729
fix(engine): anchor a Skia chain independently of where OriginalBound…
yuto-trd Aug 19, 2026
48318a6
fix(engine)!: fuse a stack of built-in Skia filters, not just a singl…
yuto-trd Aug 19, 2026
904e693
fix(engine): align the transform gizmo against the box the renderer d…
yuto-trd Aug 19, 2026
70479b1
fix(engine)!: apply a pending Skia colour filter exactly once
yuto-trd Aug 19, 2026
3d65b63
test(engine): pin the drawable-brush fit to its fractional content bo…
yuto-trd Aug 19, 2026
4be97a8
test(engine): give the delayed-nested-brush fixture the tests it was …
yuto-trd Aug 19, 2026
a619d80
fix(engine)!: rasterize a custom effect's input on the grid it crops on
yuto-trd Aug 19, 2026
4673e2a
fix(tests): bound the filter-layer poison guard to the apron the laye…
yuto-trd Aug 20, 2026
3c33795
refactor(engine)!: publish one rectangle-bounds map, the one perspect…
yuto-trd Aug 20, 2026
d803801
docs(specs): document the remaining breaking changes and retire the w…
yuto-trd Aug 20, 2026
47bd883
docs(engine): state that a materialized drawable-brush image transfer…
yuto-trd Aug 20, 2026
1ecec31
feat(engine): let a public FilterEffectActivator supply a drawable-br…
yuto-trd Aug 20, 2026
816ee51
fix(engine): stop reinstalling a render node whose disposal already b…
yuto-trd Aug 20, 2026
a79f556
fix(engine): report a nested scene's frame as its query footprint
yuto-trd Aug 20, 2026
5b56d2d
fix(engine): degrade a nested-target allocation failure in preview
yuto-trd Aug 20, 2026
a9bc1af
fix(editor): keep a template preview when its content needs a target …
yuto-trd Aug 20, 2026
8ae8631
fix(engine): invalidate every cached dependent of a changed shared child
yuto-trd Aug 20, 2026
d53b155
fix(engine)!: hold the filter apron a pixel from every sheared edge
yuto-trd Aug 20, 2026
87c7464
feat(engine): let external authors map backward render density
yuto-trd Aug 20, 2026
9918ab9
fix(engine): unwind the render-pass scope when a pass body throws
yuto-trd Aug 20, 2026
120d6bd
fix(engine): keep a synchronous flush from submitting an open render …
yuto-trd Aug 20, 2026
a52b0a5
fix(engine): release the backend texture when a render target fails t…
yuto-trd Aug 20, 2026
96154ed
fix(editor): release a thumbnail resource attached to a stopped rende…
yuto-trd Aug 20, 2026
449e712
fix(engine)!: match the ChromaKey colour where its error lives
yuto-trd Aug 20, 2026
c8314e4
refactor(engine)!: name the supply map that drops backward demand
yuto-trd Aug 20, 2026
6120900
docs(specs): reconcile the 004 specification with what was built
yuto-trd Aug 20, 2026
cef2f8e
fix(engine): keep a dark colour that only its hue separates from the key
yuto-trd Aug 21, 2026
325a7f6
fix(engine): scope a render-cache opt-out to the checkpoint that aske…
yuto-trd Aug 21, 2026
832090d
fix(engine): report an unterminated SkSL block comment instead of thr…
yuto-trd Aug 21, 2026
ef20ce1
test(engine): give the rendering suites the assertions their names pr…
yuto-trd Aug 21, 2026
1498b9f
fix(engine): honour a caller's render-target factory in the legacy fi…
yuto-trd Aug 21, 2026
2dbe81d
fix(engine): close five merge-blocking render defects
yuto-trd Aug 21, 2026
6857dfa
feat(engine)!: let an opaque source declare buffer-only raster room
yuto-trd Aug 21, 2026
518b3e2
fix(engine): keep a degraded preview frame out of everything that out…
yuto-trd Aug 21, 2026
8c79620
perf(engine): stop an output identity costing what its paths enumerate
yuto-trd Aug 21, 2026
73160e0
fix(engine): stop four resources outliving or outrunning what owns them
yuto-trd Aug 22, 2026
1e79834
docs(specs): record the backend contract change and correct four stal…
yuto-trd Aug 22, 2026
9d2fc5c
fix(engine): hand back a contributed value's target as soon as it is …
yuto-trd Aug 22, 2026
2af9acf
docs(specs): record the three author-visible changes the last commits…
yuto-trd Aug 22, 2026
7de1520
feat(engine): let an authored hit test read the resource its call bound
yuto-trd Aug 22, 2026
fa533c3
feat(engine): let a whole-source shader raise the demand on what it s…
yuto-trd Aug 22, 2026
1fa4836
fix(engine): stop hit testing reporting content the request clips away
yuto-trd Aug 22, 2026
9a49126
feat(engine): let a combine declare what density each of its inputs n…
yuto-trd Aug 22, 2026
9316699
fix(engine): keep a reusable renderer's own allocations on the contex…
yuto-trd Aug 22, 2026
bdf088a
fix(engine): invalidate a container's cache when its children change
yuto-trd Aug 22, 2026
253bc4f
fix(engine): stop a target layer scope hitting outside the region it …
yuto-trd Aug 22, 2026
09a76f3
fix(engine): report the content a self-allocating path drops from a p…
yuto-trd Aug 22, 2026
def8dcb
refactor(engine)!: admit that a detached resource has no backing object
yuto-trd Aug 22, 2026
8988a5d
fix(engine): reject the Vulkan work a driver would leave undefined
yuto-trd Aug 22, 2026
7855b18
test(engine): fail non-finite parity pixels on a job that declared a GPU
yuto-trd Aug 22, 2026
db38ca8
feat(engine): let a plugin author parse a shader source once and reus…
yuto-trd Aug 22, 2026
66dc048
refactor(engine)!: make a render host state what its output is for
yuto-trd Aug 22, 2026
294ce95
docs(specs): stop the 004 record contradicting itself, and unpin the …
yuto-trd Aug 22, 2026
7e4d3b0
test(engine): fail a GPU run that reports a Vulkan validation error
yuto-trd Aug 23, 2026
17f301f
fix(engine): keep a render pass's own barriers in recording order
yuto-trd Aug 23, 2026
c9ab893
fix(engine)!: push constants for the stages the bound layout declares
yuto-trd Aug 23, 2026
79d02f0
docs(specs): count the push-constant break in the migration contract
yuto-trd Aug 23, 2026
e20b9d1
fix(engine): tell Skia the layout its image is actually in
yuto-trd Aug 23, 2026
f6285fc
fix(engine): request the cube-array feature point-light shadows depen…
yuto-trd Aug 23, 2026
1ae2195
fix(engine): make every allocated shadow slot readable before anythin…
yuto-trd Aug 23, 2026
6f439b1
fix(engine): make an array texture state the usage its format supports
yuto-trd Aug 23, 2026
45244fe
docs: point the validation-gate exclusion at its tracking issue
yuto-trd Aug 23, 2026
114e26f
fix(engine): release a texture's handles when its initial transition …
yuto-trd Aug 23, 2026
99ad6e1
fix(engine): let a Skia clear settle the backend's transparency record
yuto-trd Aug 23, 2026
2c62494
fix(app): export a selected element whole, not the part the frame shows
yuto-trd Aug 23, 2026
c67dcf5
fix(engine): ask every target scope what density its input needs
yuto-trd Aug 23, 2026
104529d
feat(engine): let a geometry operation say what density its input needs
yuto-trd Aug 23, 2026
5a12139
feat(engine): let a target command say what density each of its input…
yuto-trd Aug 23, 2026
d58ef7c
docs(specs): record where an input-demand contract can now be declared
yuto-trd Aug 23, 2026
5f5fd86
fix(engine): keep an append scope's input at the density its destinat…
yuto-trd Aug 23, 2026
749c944
docs(specs): say where an input-demand contract has no equivalent
yuto-trd Aug 23, 2026
bd69026
fix(engine): flush the context the target being sampled actually live…
yuto-trd Aug 23, 2026
fad57f2
fix(engine): reject a metadata capture the author can still reassign
yuto-trd Aug 23, 2026
f12d459
fix(engine): let a contract be recorded from inside another lambda
yuto-trd Aug 23, 2026
42e3e0e
fix(engine): settle a capture by its declared type instead of reading it
yuto-trd Aug 23, 2026
155cbc3
fix(engine): carry a raw scope's declared demand back to its input
yuto-trd Aug 23, 2026
ea614fe
fix(engine): keep a group's full-target work when it isolates
yuto-trd Aug 23, 2026
2fa6ed4
feat(engine): let a raw session address a resource by its declared slot
yuto-trd Aug 23, 2026
8d02d5e
fix(engine): follow what a captured read-only view points at
yuto-trd Aug 23, 2026
20dfe84
fix(engine): fix an immutable collection by what it holds, not by its…
yuto-trd Aug 23, 2026
5ade9db
feat(engine)!: let a scope declare the space its replay transform liv…
yuto-trd Aug 23, 2026
41206bc
fix(engine): rebuild a nested scene at the density the request asks for
yuto-trd Aug 23, 2026
788d2be
fix(engine): declare a perspective transform's bounds where its outpu…
yuto-trd Aug 23, 2026
d685776
fix(engine): split a render pass around the work Vulkan forbids insid…
yuto-trd Aug 23, 2026
e287003
fix(engine): prepare a node reached with explicit inputs too
yuto-trd Aug 23, 2026
5932544
fix(engine): tell the request when a native replacement was declined
yuto-trd Aug 23, 2026
b4e2366
fix(engine): stop clearing a target its consumer is about to overwrite
yuto-trd Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,4 @@ telemetry-data.json

# `.mcp.json` is team-shared (see docs/ai-workflow/README.md); override any global gitignore
!.mcp.json

2 changes: 1 addition & 1 deletion .specify/memory/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Beutl targets `net10.0` and `net10.0-windows`. Both targets must keep building.

### III. Test-First with NUnit

- Test framework is NUnit + Moq. Tests are organized under `tests/` in per-area projects (e.g. `tests/Beutl.UnitTests/`, `tests/Beutl.Graphics3DTests/`, `tests/SourceGeneratorTest/`, `tests/Beutl.FFmpegIpc.Tests/`). `tests/Beutl.Graphics3DTests/` is a Vulkan-gated NUnit suite that self-skips when no Vulkan device is available.
- Test framework is NUnit + Moq. Tests are organized under `tests/` in per-area projects (e.g. `tests/Beutl.UnitTests/`, `tests/Beutl.PublicApiContractTests/`, `tests/Beutl.Graphics3DTests/`, `tests/SourceGeneratorTest/`, `tests/Beutl.FFmpegIpc.Tests/`). `tests/Beutl.PublicApiContractTests/` is the non-friend compile gate for public authoring APIs; `tests/Beutl.Graphics3DTests/` is a Vulkan-gated NUnit suite that self-skips when no Vulkan device is available.
- New logic in `src/` is incomplete without an accompanying test.
- Benchmarks (`tests/Beutl.Benchmarks`, `tests/Beutl.FFmpegBenchmarks`) use BenchmarkDotNet and are excluded from regular `dotnet test` runs.
- CI quality gate: `dotnet test Beutl.slnx -f net10.0 --settings coverlet.runsettings` must pass, with the coverage threshold configured in [`.github/workflows/dotnet.yml`](../../.github/workflows/dotnet.yml) honored.
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Beutl is a cross-platform video editing / compositing application built on Avalo

- License: the main app is **MIT**; `Beutl.FFmpegWorker` alone is **GPL-3.0-or-later** (a separate process)
- UI: Avalonia (XAML + ViewModel)
- Tests: NUnit + Moq under `tests/` (per-area projects, e.g. `tests/Beutl.UnitTests/`, `tests/Beutl.Graphics3DTests/`, `tests/SourceGeneratorTest/`, `tests/Beutl.FFmpegIpc.Tests/`). `tests/Beutl.Graphics3DTests/` is a Vulkan-gated NUnit suite that self-skips when no Vulkan device is available — see `tests/CLAUDE.md`
- Tests: NUnit + Moq under `tests/` (per-area projects, e.g. `tests/Beutl.UnitTests/`, `tests/Beutl.PublicApiContractTests/`, `tests/Beutl.Graphics3DTests/`, `tests/SourceGeneratorTest/`, `tests/Beutl.FFmpegIpc.Tests/`). `tests/Beutl.PublicApiContractTests/` is the non-friend compile gate for public authoring APIs; `tests/Beutl.Graphics3DTests/` is a Vulkan-gated NUnit suite that self-skips when no Vulkan device is available — see `tests/CLAUDE.md`
- E2E / headless-UI tests: `tests/Beutl.E2ETests/` (library-level) and `tests/Beutl.HeadlessUITests/` (drives the real shell, the sole test referencing `src/Beutl`) on `Avalonia.Headless.NUnit`, with shared helpers in `tests/Beutl.Testing.Headless/`. They run on headless CI without xvfb or a GPU — see `tests/CLAUDE.md`
- Build: Nuke (`nukebuild/`) or `dotnet` directly

Expand Down
1 change: 1 addition & 0 deletions Beutl.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<Project Path="tests\Beutl.UnitTests\Beutl.UnitTests.csproj" />
<Project Path="tests\Beutl.E2ETests\Beutl.E2ETests.csproj" />
<Project Path="tests\Beutl.HeadlessUITests\Beutl.HeadlessUITests.csproj" />
<Project Path="tests\Beutl.PublicApiContractTests\Beutl.PublicApiContractTests.csproj" />
<Project Path="tests\Beutl.Testing.Headless\Beutl.Testing.Headless.csproj" />
<Project Path="tests\SourceGeneratorTest\SourceGeneratorTest.csproj" />
<File Path="tests\ArtifactProvider.cs" />
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

**Feature**: 003 | Consumers: `Beutl.NodeGraph`, `Beutl.Extensibility`, out-of-tree plugin drawables/effects, `Beutl.ProjectSystem`, `Beutl` (editor).

> **Superseded API note (Feature 004):** the symbol table, call-site inventory, and downstream
> migration summary below record the Feature 003 API at the time it shipped. Feature 004 removes
> `RenderNodeOperation`/`RenderNodeProcessor` and revises the filter-effect compatibility surface.
> Use Feature 004's [breaking-change contract](../../004-gpu-pass-fusion/contracts/breaking-changes.md)
> and [public API contract](../../004-gpu-pass-fusion/contracts/public-api.md) for current migration
> instructions. For current author guidance, use
> [Resolution-independent rendering](../../../ai-workflow/resolution-independent-rendering.md).

This feature ships as a **breaking change**: `refactor!:` / `feat!:` with a `BREAKING CHANGE:` footer naming `Beutl.Engine`, `Beutl.NodeGraph`, `Beutl.ProjectSystem`. **No `[Obsolete]` shims** (AGENTS.md); all in-tree call sites updated in the same change. Route through `beutl-design-reviewer` (FR-028). **No file-format change** (FR-001/SC-002).

## Breaking symbols
Expand All @@ -11,14 +19,14 @@ This feature ships as a **breaking change**: `refactor!:` / `feat!:` with a `BRE
| `RenderNodeContext` ctor | `Rendering/RenderNodeContext.cs:3` | `ctor(RenderNodeOperation[] input)` | `ctor(input, float outputScale = 1f, float maxWorkingScale = +∞)` + `float OutputScale { get; }` + `float MaxWorkingScale { get; }` + `static float ResolveWorkingScale(inputs, outputScale, maxWorkingScale)` (FR-036 floor rule `min(max(s_out, supply), max)`, **no `policy` param**). The ctor **sanitizes degenerate inputs once** so downstream consumers (effects, particles, 3D) need not re-validate: a non-finite / non-positive `OutputScale` (`0`/`NaN`/`∞`) → `1`, a `NaN` / non-positive `MaxWorkingScale` → `+∞` (no ceiling). | Sole construction at `RenderNodeProcessor.cs:176`; reaches ~28 `Process` overrides (FR-004) |
| `RenderNodeOperation` | `Rendering/RenderNodeOperation.cs:6-64` | 4 static factories, `Bounds`/`Render`/`HitTest` | + `EffectiveScale EffectiveScale { get; }` (**value type**, default `Unbounded`); factories gain `EffectiveScale effectiveScale = default`. **No `LosslessReRasterizable` bool** — `IsUnbounded` subsumes it | Subclasses + `FilterEffectRenderNode.cs:47,73`, `RenderNodeProcessor.cs:102`, `ParticleRenderNode.cs:83`, `SceneDrawable.cs:185` |
| `EffectiveScale` (new type) | `Rendering/EffectiveScale.cs` | — | **As shipped:** a non-positional `readonly record struct` with a private inverted `_bounded` flag (NOT the positional `(float Value, bool IsUnbounded)` form — that would make `default` wrongly `At(0)`); `Unbounded`/`At(float)`/**`AtOrUnbounded(float)`**/`IsUnbounded`/`Value`; `default == Unbounded`. **`At(scale)` THROWS** `ArgumentOutOfRangeException` on a non-finite / non-positive density and the render pull path has **no try/catch** (a throw aborts the render), so a plugin `RenderNodeOperation`/`EffectiveScale` override deriving a density from animatable geometry MUST pre-guard the quotient or use the non-throwing **`AtOrUnbounded`** (degrades a bad density to `Unbounded`). | FR-018; produced by flushed effect buffers (`At(w)`), `CreateTarget`, 3D surfaces; consumed by `EffectTarget.Draw` + the op factories |
| ~~`ResolutionPolicy` (new type)~~ **NOT shipped** | — | — | **Removed.** Earlier drafts added a `readonly record struct ResolutionPolicy` (`Inherit`/`ClampToOutput`/`Oversample(k)`, `PreserveSource` already dropped) + a `virtual FilterEffect.ResolutionPolicy`. No built-in needed a non-default value and a custom `FilterEffectRenderNode` is strictly more flexible, so the type, the virtual, the never-added `RenderNode.ResolutionPolicy`, and the `ResolveWorkingScale` `policy` param were all removed. An effect needing a non-supply `w` overrides `Process` in a node from `FilterEffect.Resource.CreateRenderNode()`. | FR-036 |
| ~~`ResolutionPolicy` (new type)~~ **NOT shipped** | — | — | **Removed.** Earlier drafts added a `readonly record struct ResolutionPolicy` (`Inherit`/`ClampToOutput`/`Oversample(k)`, `PreserveSource` already dropped) + a `virtual FilterEffect.ResolutionPolicy`. No built-in needed a non-default value, so the type, the virtual, the never-added `RenderNode.ResolutionPolicy`, and the `ResolveWorkingScale` `policy` param were removed. An effect needing a non-supply `w` overrides `GetWorkingScaleContract()` in a node from `FilterEffect.Resource.CreateRenderNode()`; overriding `Process` is reserved for genuinely different topology/lowering. | FR-036 |
| `RenderNodeProcessor` ctor | `Rendering/RenderNodeProcessor.cs:6` | `ctor(RenderNode root, bool useRenderCache)` | `ctor(root, useRenderCache, float outputScale = 1f, float maxWorkingScale = +∞)` + `float OutputScale { get; }` + `float MaxWorkingScale { get; }`; rasterization sinks rasterize at `OutputScale` (`PixelRect.FromRect(bounds, OutputScale)`); the per-input working scale is resolved at `FilterEffectRenderNode` | `Renderer.cs:214`, `NodeGraphFilterEffectRenderNode.cs:45`, `ParticleRenderNode.cs:144` |
| `Renderer` ctor | `Rendering/Renderer.cs:45` | `ctor(int width, int height)` | `ctor(int width, int height, float renderScale = 1f, float maxWorkingScale = float.PositiveInfinity)` (the output scale `s_out` + the FR-037 ceiling) + `OutputScale`/`DeviceSize` getters; width/height **logical**, surface `ceil(FrameSize×s_out)` | FR-003/FR-026/FR-037; `SceneDrawable.cs:181`, `EditViewModel.cs:75`, `OutputViewModel.cs:283` |
| `IRenderer` | `Rendering/IRenderer.cs:7` | — | + `float OutputScale { get; }`, + `PixelSize DeviceSize { get; }` (default-interface-impl → `1f`/`FrameSize` to soften third-party impls, mirroring `GetBoundary` default at `:30`) | third-party `IRenderer` implementers |
| `SceneRenderer` ctor | `ProjectSystem/SceneRenderer.cs:10` | `ctor(Scene scene, bool disableResourceShare = false)` | `ctor(Scene scene, float renderScale = 1f, bool disableResourceShare = false, float maxWorkingScale = float.PositiveInfinity)` | `EditViewModel.cs:75` (passes `maxWorkingScale: 2f * s_out`), `OutputViewModel.cs:283` (passes `+∞` — export imposes no working-scale quality ceiling, *amended 2026-06-15*; was `max(8, 4 × s_out)`) |
| `GraphicsContext2D` ctor + `Size` | `Rendering/GraphicsContext2D.cs:9` | `ctor(ContainerRenderNode, PixelSize canvasSize = default)`; `PixelSize Size` | **As shipped:** `ctor(ContainerRenderNode, Size canvasSize = default, float outputScale = 1f)` + `OutputScale`; **`Size` is now an exact logical `Size`** (float), not a rounded `PixelSize` — it feeds `Drawable.Render`'s `MeasureCore`/`GetTransformMatrix` where a fractional viewport changes placement, so readers drop the `.ToSize(1)`. `DrawBackdrop` uses `new Rect(canvasSize)` | FR-021; **most-consumed changed ctor** — full call-site list below |
| `ImmediateCanvas` / `ICanvas` (**density-aware logical surface**) | `Graphics/ImmediateCanvas.cs`, `Graphics/ICanvas.cs` | `ctor(RenderTarget, float outputScale = 1f, float maxWorkingScale = +∞)`; `PixelSize Size`; `float OutputScale`; public `Matrix Transform { get; set; }` | **As shipped (base-CTM redefinition):** the canvas **bakes** the base CTM `CreateScale(density)` at construction (true no-op at density 1). `ctor(RenderTarget, float density = 1f, float maxWorkingScale = +∞, Size logicalSize = default)`; `ICanvas.Size` → **`LogicalSize` (`Size`) + `DeviceSize` (`PixelSize`)**; `OutputScale` → **`SurfaceDensity`** (immutable, drives base CTM + snapshot capture) + **`Density`** (current, push/pop, drives brush fills + nested pulls); new **`PushDeviceSpace()`** (CTM → identity, density → 1) on `ICanvas`; `Transform` **setter is `internal`** (getter stays public and now includes the base) | feature 003 base-CTM redefinition; all `FilterEffect`/`CustomEffect` authors + `ICanvas` implementers |
| `FilterEffectContext` | `FilterEffects/FilterEffectContext.cs:39` | no scale | + ctor `(outputScale, workingScale)` + `float WorkingScale { get; }` + `float OutputScale { get; }`; **CustomEffect point-blit** code × **`WorkingScale`**, Skia `SKImageFilter` primitives ride the `CreateScale(w)` CTM (NOT multiplied) | FR-009/FR-015; `FilterEffectRenderNode.cs:30` |
| `FilterEffectContext` | `FilterEffects/FilterEffectContext.cs:39` | no scale | + ctor `(outputScale, workingScale)` + `bool TryGetWorkingScale(out float)` + `float WorkingScale { get; }` + `float OutputScale { get; }`; author-time working scale is unavailable for symbolic/branch-dependent inputs (`TryGet... == false`, getter throws), and available values are nominal before operation-specific bounds clamps; **CustomEffect point-blit** code uses its execution-time density, while Skia `SKImageFilter` primitives ride the `CreateScale(w)` CTM (NOT multiplied) | FR-009/FR-015; `FilterEffectRenderNode.cs:30` |
| `CustomFilterEffectContext` | `FilterEffects/CustomFilterEffectContext.cs` | `CreateTarget` `(int)bounds.W/H` | + `float WorkingScale { get; }` + `float OutputScale { get; }` (forwarded for nested re-application); `CreateTarget` `ceil(bounds×WorkingScale)`; **`Open` returns a canvas with the baked base CTM `CreateScale(target.Scale.Value)`** (the author no longer pushes `CreateScale(WorkingScale)`); `DeviceBufferSize(bounds, w)` is public so custom effect authors can compute shader/device uniforms with the same scale-1.0-sensitive rounding as `CreateTarget` | FR-009/FR-015; migrate off `(int)` cast (scale-1.0-sensitive) |
| `FilterEffectActivator` | `FilterEffects/FilterEffectActivator.cs` | `ctor(targets, builder, workingScale = 1f)` | + `float outputScale = 1f` **and `float maxWorkingScale = +∞`** ctor params + `OutputScale` / `MaxWorkingScale` getters (forwarded into the nested `FilterEffectContext`/`CustomFilterEffectContext` so nested pulls stay under the FR-037 ceiling); `Flush` sizes `ceil(×w)`, the **flatten canvas bakes the base CTM `CreateScale(w)`** (translation-only push), tags buffers `At(w)` | FR-009/FR-015/FR-019/FR-037; scale-1.0-sensitive |
| `EffectTarget` | `FilterEffects/EffectTarget.cs:6` | `Empty`/`Size` (obsolete) | + `EffectiveScale Scale { get; set; }` (default `Unbounded`); **remove** `Empty`/`Size` | FR-019; LayerEffect mixed-scale detection (dossier §4.5) |
Expand Down
Loading