Skip to content

feat(canvas): noise adjustment for film grain - #9933

Open
joshistoast wants to merge 2 commits into
mainfrom
feat/canvas-noise-adjustment
Open

joshistoast wants to merge 2 commits into
mainfrom
feat/canvas-noise-adjustment

Conversation

@joshistoast

@joshistoast joshistoast commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a Noise adjustment for raster layers and groups, aimed at adding film grain.

  • Distribution: Gaussian or Uniform. Both give the same standard deviation at the same amount, so switching changes grain character, not strength.
  • Grain: Monochromatic (one value shared by R, G, B) or Color (independent per channel).
  • Amount: 0–100%, default 10%. σ = amount × 128 on the 0–255 scale.
  • New pattern: a dice button re-rolls the seed; undoable. Duplicating a noise adjustment gets a fresh seed; duplicating a layer keeps it.

Noise is a deterministic hash of seed, pixel position and channel. A layer's grain lives in layer-local pixels and moves, scales and rotates with the layer; a group's grain is anchored to document pixels. applyAdjustments now takes the buffer's origin, and every caller passes one, so partial redraws match full ones.

To keep grain identical everywhere, a layer's own adjustments now run on layer-local pixels before resampling on every path: display, generation composite, composite export, merge/crop/copy bakes and PSD. Noise stacks no longer take the adjust-after-resampling shortcut in composites. Bakes and PSD do this for all stacks, so scaled or rotated layers with existing adjustments now bake exactly what the canvas shows. Bakes reuse the display's adjusted copy when it is current, which avoids extra memory. Thumbnails reuse it too.

QA Instructions

  • pnpm check:release in invokeai/frontend/webv2: passed. Covers lint, unit, browser, fixtures, architecture and browser performance gates, project-file journeys and accessibility.
  • Manual check against the mock backend (pnpm dev:mock):
    1. Right-click a raster layer, choose Add adjustment → Noise. The row reads "Noise 10% · Mono".
    2. In Properties, switch Distribution and Grain, scrub Amount, and press the dice button. Each edit updates the canvas and undoes cleanly.
    3. Repeat on a layer group.
  • Noise pass cost, Node, median, 4096² ImageData:
Stack ms
LUT only 127–134
Noise, Gaussian mono 163
Noise, Gaussian color 188
  • Generation composite of a 4096² noisy layer into a 1024² output, Chromium, median: 8 ms at scale 1, 27 ms at scale 0.5, 116 ms at scale 0.25. Cost now follows the sampled layer region instead of the output.

Review

Resolved:

  • Grain strength used to differ from the display for scaled layers in generation, bakes and PSD, and changed with bbox placement. Fixed by the layer-local-first rule above.
  • Bakes reserved a layer-sized copy for every adjusted stack. They now reuse the display's current copy.
  • Added noise coverage for group surface regions, PSD group/layer stacks, floating selections, thumbnails and re-roll undo.
  • Renamed the ambiguous "Color: Monochromatic | Color" field to "Grain".

Remaining limits:

  • Generating with a very large, heavily downscaled noisy layer can return over-budget. Generation reads a detached snapshot, so it cannot reuse the display copy.
  • A merge, crop or copy of a large downscaled adjusted layer with no current display copy reserves a layer-sized copy and can return over-budget where it previously succeeded.
  • PSD cannot represent group adjustments, so a group's noise is baked onto each member separately. It can differ where semi-transparent members overlap.
  • A dragged floating selection keeps the grain it had when lifted until it is placed.
  • Thumbnails without a current display copy approximate the grain by scaling the amount.

Compatibility / Rollout

  • Adds a noise arm to the persisted adjustment schema. Existing documents load unchanged.
  • An older build that opens a document with a noise entry drops that layer's whole adjustment stack, and its next save makes that permanent. Earlier adjustment types such as exposure and channel-scoped levels behaved the same way.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Adds a Noise raster/group adjustment with Gaussian or Uniform distribution, monochromatic or color grain, amount, and a re-rollable seed.
Noise is a deterministic function of position, so display, generation, export, merges and PSD share the same grain.
Layer bakes and PSD now apply a layer's own adjustments before resampling, matching the display.
@github-actions github-actions Bot added frontend PRs that change frontend files docs PRs that change docs labels Oct 19, 2026

@JPPhoto JPPhoto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Merge blockers

  1. [P1 · Must fix] Merge Down drops Noise. mergeLayerController.ts:139 composites raw cache pixels; the reducer removes the layer adjustments. Impact / recovery: Grain disappears from either adjusted layer; undo is the recovery. Test: Add a Noise-specific pre/post Merge Down pixel comparison. Fix: Bake adjustments before compositing. If skipped: Merge Down violates the PR’s promise that merges preserve grain.

  2. [P1 · Must fix] Older builds can erase the whole adjustment stack. canvasMigration.ts:125 accepts Noise, while the writer still emits schema 3 or 4. An older client can drop the unknown stack on load, then autosave that loss. Impact / recovery: Permanent after the save. Test: Open and save a Noise document with an older schema reader. Fix: Mark Noise documents with a schema version older builds refuse. The PR documents this limitation, but it remains a data-loss path.

Other findings/issues

  1. [P2 · Should fix] Rasterize changes the Noise pattern. rasterizeLayerController.ts:86 transforms raw parametric pixels, resets the transform, then keeps the adjustment. Noise runs on a new pixel grid. Impact / recovery: Grain changes after rasterizing a transformed text, shape or gradient layer; undo recovers it. Test: Compare pixels before and after rasterizing a transformed noisy layer. Fix: Apply Noise before baking the transform.

  2. [P2 · Should fix] Text noise can exceed its memory reservation. rasterComposite.ts:247 reserves from estimated text bounds, while rasterization uses browser-measured bounds. Wide text under heavy downscaling can allocate a larger scratch region than reserved. Impact / likelihood: Possible budget overrun or browser allocation failure with wide text and tight memory. Test: Use text whose measured bounds exceed the estimate. Fix: Reserve from actual layer bounds or a conservative text bound.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs PRs that change docs frontend PRs that change frontend files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants