Skip to content

Unify mirage + blur nodes onto one EffectNode spine - #151

Merged
l2hyunwoo merged 19 commits into
mainfrom
refactor/effect-node-pipeline
Jul 12, 2026
Merged

Unify mirage + blur nodes onto one EffectNode spine#151
l2hyunwoo merged 19 commits into
mainfrom
refactor/effect-node-pipeline

Conversation

@l2hyunwoo

@l2hyunwoo l2hyunwoo commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Unifies the four separate modifier nodes behind Modifier.mirage, Modifier.mirage(sky), Modifier.cloudy(radius), and Modifier.cloudy(sky, ...) into one EffectNode spine. Before this branch, the mirage self-content node and the mirage backdrop node were ~90% duplicated, and the foreground/background blur ran through separate strategy/factory classes. They now share one Modifier.Node that owns the plan, clock, layer pool, positioning, and post-processing, and delegates the per-effect draw to an Effect (either MirageEffect for the shader pipeline or BlurStrategy for the platform blur).

No public API change. Modifier.mirage / Modifier.cloudy / Modifier.sky / liquidGlass / CloudyState and their KDoc contracts are unchanged; the klib and JVM ABI dumps show only internal-symbol churn.

This is opened as a draft: it is the first slice of a larger unification (a shared forecast { } plan DSL, a light rig, and the liquidGlass strangle are planned on top of this spine), and I'd like a read on the internal shape before building further.

What changed

  • EffectNode (commonMain, internal) replaces MirageNode + MirageBackdropNode + the two blur nodes. It branches on a nullable Sky? for the stage-0 source (null = self content, non-null = backdrop).
  • Effect interface — two implementers: MirageEffect (shader filter/overlay pipeline) and BlurStrategy (android + skiko). The Android blur resolves a BlurTier per draw: GPU RenderEffect (API 31+), CPU legacy blur (API < 31 with cpuBlurEnabled), or scrim (API < 31 without). The legacy CPU blur machines are extracted from the old nodes unchanged, not rewritten.
  • MirageFilterChain (the layer pool + stage chaining) is owned by the node and used by MirageEffect; blur bypasses it.
  • Vocabulary: the internal types use standard render-pipeline names (Effect, EffectNode, SourceSky?, BlurTier, Stage, MirageFilterChain) rather than domain metaphors, so the internals read the way a graphics reviewer expects. The public concept names (mirage, sky, cloudy) stay.

Included fix — backdrop captureToImage / PixelCopy crash

While building the test safety net for this refactor I found a pre-existing crash (reproduces on unmodified main, tracked separately in #150): captureToImage() of a Modifier.sky tree with a descendant Modifier.cloudy(sky = ...) backdrop SIGSEGVs the RenderThread with an unbounded RenderNode::prepareTreeImpl recursion. The backdrop blur records drawLayer(sky.backgroundLayer), and since the backdrop node is a descendant of the sky recorder, that closes a cyclic RenderNode reference. On-screen rendering survives it (the Sky.isCapturing guard, damage-scoped walk), but the full-tree re-walk captureToImage forces does not. The fix samples the sky through a rasterized snapshot (drawImage) instead of the live layer, which is the acyclic structure the API < 31 CPU path already used. It carries onto this branch as commit 6a42ed6.

Test plan

  • New SkyBackdropRasterTest (desktopTest, ImageComposeScene) — the first test that executes the skiko backdrop render path, asserting blur / tint / mirage pixel effects.
  • New Issue112RegressionTest + BackdropCaptureCrashRegressionTest (instrumented) — the four capture variants that crashed before the fix now pass.
  • MirageElementEqualityTest, MiragePlanModifierTest, MirageFilterChainTest, MirageChromaticRasterTest updated to the unified types, intent unchanged.
  • Instrumented suite green on emulator API 34, emulator API 27 (exercises the CPU blur and scrim tiers), and a real device on API 36.
  • desktopTest, testAndroidHostTest, spotlessCheck, checkLegacyAbi green. iosArm64 + wasmJs compile (commonMain purity).

Summary by CodeRabbit

  • New Features
    • Introduced the shader-based Mirage API and preset catalog, shifting from optic/plan language to shader/pipeline.
    • Updated the Mirage demo and examples to use shader pipelines, filters, and overlays.
    • Unified cloudy blur rendering behavior across platforms with a consistent effect-based pipeline.
  • Bug Fixes
    • Prevented capture-related cyclic rendering crashes (including animated and repeated captures).
    • Improved reliability of backdrop and blur/tint rendering across cases.
  • Documentation
    • Updated in-app and online docs/snippets to use the new shader and pipeline terminology.
  • Tests
    • Added/updated regression, screenshot, raster, and shader compilation test coverage for the new pipeline.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3bef7602-b01f-49db-9161-bd30a013bded

📥 Commits

Reviewing files that changed from the base of the PR and between 9f67d52 and 82b920f.

⛔ Files ignored due to path filters (1)
  • cloudy/api/cloudy.klib.api is excluded by !cloudy/api/**/*.api
📒 Files selected for processing (76)
  • app/src/androidMain/kotlin/demo/shader/DropletMapImage.android.kt
  • app/src/commonMain/kotlin/demo/Route.kt
  • app/src/commonMain/kotlin/demo/screen/MenuHomeScreen.kt
  • app/src/commonMain/kotlin/demo/screen/MirageScreen.kt
  • app/src/commonMain/kotlin/demo/screen/MirageSkyScreen.kt
  • app/src/commonMain/kotlin/demo/shader/DropletMap.kt
  • app/src/commonMain/kotlin/demo/shader/RainyWindowShader.kt
  • app/src/skikoMain/kotlin/demo/shader/DropletMapImage.skiko.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/BackdropCaptureCrashRegressionTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlProgramMatchTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlesRoundtripBenchmark.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/Issue112RegressionTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MainPixelCopyCrashReproTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MirageBandScreenshotTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MirageFilterChainTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MiragePipelineModifierTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/ScreenshotTestSupport.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBackground.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/MirageModifier.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BackdropClearBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurTier.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyBackdropBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyForegroundBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageBackendProgram.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageNode.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/ExperimentalMirage.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlass.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlassExperimental.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageModifier.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageParams.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShader.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShaders.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/CompiledProgram.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/Effect.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectElement.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropElement.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropSnapshot.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackend.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageColorGrade.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageCompiler.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageEffect.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageFilterChain.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageGlslEs.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageKernels.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MiragePipeline.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MiragePreamble.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageUniformBinding.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/PostProcess.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageCompilerTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageElementEqualityTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageGlslEsTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MiragePresetTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageChromaticRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageColorGradeRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageProgramCacheTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/RainyWindowRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/Cloudy.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/CloudyBackground.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/MirageModifier.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/MirageNode.skiko.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/ApiMirageScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/GettingStartedScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/HomeScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/PlaygroundScreen.kt
💤 Files with no reviewable changes (7)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MainPixelCopyCrashReproTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropElement.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropNode.kt
🚧 Files skipped from review as they are similar to previous changes (66)
  • app/src/skikoMain/kotlin/demo/shader/DropletMapImage.skiko.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageParams.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/ScreenshotTestSupport.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/MirageNode.skiko.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/MirageModifier.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageNode.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MiragePreamble.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlass.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/Effect.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackend.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageEffect.kt
  • app/src/androidMain/kotlin/demo/shader/DropletMapImage.android.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MirageFilterChainTest.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurTier.android.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/MirageModifier.skiko.kt
  • app/src/commonMain/kotlin/demo/screen/MenuHomeScreen.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageBackendProgram.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectElement.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageColorGradeRasterTest.kt
  • app/src/commonMain/kotlin/demo/shader/DropletMap.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/Cloudy.skiko.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/PostProcess.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageColorGrade.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlesRoundtripBenchmark.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.skiko.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlassExperimental.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageFilterChain.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/Issue112RegressionTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageGlslEs.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/HomeScreen.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageElementEqualityTest.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • app/src/commonMain/kotlin/demo/shader/RainyWindowShader.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/ExperimentalMirage.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageProgramCacheTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageGlslEsTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/RainyWindowRasterTest.kt
  • app/src/commonMain/kotlin/demo/Route.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/GettingStartedScreen.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/BackdropCaptureCrashRegressionTest.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/ApiMirageScreen.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageKernels.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageModifier.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageChromaticRasterTest.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyForegroundBlurrer.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/PlaygroundScreen.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageCompiler.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/CompiledProgram.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BackdropClearBlurrer.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MiragePresetTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageUniformBinding.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShaders.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlProgramMatchTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropSnapshot.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MiragePipelineModifierTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectNode.kt
  • app/src/commonMain/kotlin/demo/screen/MirageSkyScreen.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyBackdropBlurrer.kt
  • app/src/commonMain/kotlin/demo/screen/MirageScreen.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageCompilerTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShader.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/CloudyBackground.skiko.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt

📝 Walkthrough

Walkthrough

The PR migrates Mirage from optic-based plans to shader-based pipelines, introduces unified effect rendering, replaces platform blur routing, adds Android acyclic backdrop capture, and expands screenshot, raster, compiler, and crash-regression coverage.

Changes

Mirage shader and effect migration

Layer / File(s) Summary
Shader API and pipeline contracts
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShader.kt, MirageShaders.kt, MirageModifier.kt, internal/MiragePipeline.kt
Public optic types, categories, presets, modifier terminology, and pipeline stages are migrated to shader-based contracts.
Unified effect rendering
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/Effect*.kt, MirageEffect.kt, EffectNode.kt, PostProcess.kt
A shared effect architecture handles reconciliation, backdrop sampling, filter execution, overlays, clocks, post-processing, and lifecycle cleanup.
Platform blur and crash-safe capture
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/**, cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/**
Foreground and backdrop blur route through platform-specific BlurStrategy implementations; Android uses acyclic snapshots for backdrop capture, while Skiko uses the unified GPU path.
Validation and adoption
cloudy/src/*Test/**, app/src/**, docs/src/**
Tests, demos, documentation, compiler checks, raster assertions, screenshot specs, and RenderNode crash regressions adopt the shader catalog and pipeline terminology.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Modifier.mirage
  participant EffectNode
  participant BlurStrategy
  participant MirageBackdropSnapshot
  participant MirageEffect
  Modifier.mirage->>EffectNode: Build EffectElement and pipeline stages
  EffectNode->>BlurStrategy: Draw platform blur stage
  BlurStrategy->>MirageBackdropSnapshot: Capture acyclic backdrop snapshot on Android
  EffectNode->>MirageEffect: Draw shader filters and overlays
  MirageEffect->>EffectNode: Report rendered state
Loading

Possibly related PRs

  • skydoves/Cloudy#121: Reworks the legacy Android CPU blur path that this PR removes and replaces.
  • skydoves/Cloudy#128: Introduces the Mirage optic-plan area migrated here to shader pipelines.
  • skydoves/Cloudy#150: Covers the overlapping captureToImage() and cyclic RenderNode crash path.

Suggested reviewers: skydoves

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the refactor and tests, but it misses several required template sections and the requested example walkthrough. Add the required 🎯 Goal, 🛠 Implementation details, ✍️ Explain examples, and Preparing a pull request for review sections, including example snippets and verification steps.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.12% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactor: consolidating mirage and blur nodes onto a single EffectNode spine.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/effect-node-pipeline

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@l2hyunwoo
l2hyunwoo force-pushed the refactor/effect-node-pipeline branch from 2e803cd to 325d31c Compare July 11, 2026 01:15
github-actions Bot added a commit that referenced this pull request Jul 11, 2026
@l2hyunwoo
l2hyunwoo force-pushed the refactor/effect-node-pipeline branch 3 times, most recently from 7c041ab to 1da0c72 Compare July 11, 2026 23:39
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown

Snapshot diff report

✅ 8 screenshots verified, no changes.

Total Unchanged Changed Added
8 8 0 0

Full screenshot report (artifact)

Screenshots

Committed reference screenshots each spec verifies against (current expected state).

File name Image
passthrough_radius8.png
smoke_radius0.png
passthrough_radius16.png
smoke_radius24.png
state_disabled.png
state_enabled.png
passthrough_radius0.png
passthrough_radius24.png

Device rendering

Actual pixels captured on the emulator per API band (API 30 = GLES mirage + legacy blur, API 34 = AGSL + RenderEffect). Render evidence, not a golden diff.

Band Case Actual Expected
API 30 blur
API 30 chromatic (none)
API 30 duotone
API 34 blur
API 34 chromatic (none)
API 34 duotone

github-actions Bot added a commit that referenced this pull request Jul 11, 2026
@l2hyunwoo
l2hyunwoo force-pushed the refactor/effect-node-pipeline branch from 1da0c72 to 9f67d52 Compare July 12, 2026 01:54
github-actions Bot added a commit that referenced this pull request Jul 12, 2026
@l2hyunwoo
l2hyunwoo marked this pull request as ready for review July 12, 2026 09:32
@l2hyunwoo
l2hyunwoo requested a review from skydoves as a code owner July 12, 2026 09:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt (1)

120-129: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider renaming planRenders to pipelineRenders.

The function name still uses the old "plan" vocabulary while the rest of the codebase has migrated to "pipeline." The when dispatch on Stage subtypes is correct — PlatformFilter returning false is the right behavior since blur stages render on their own path and shouldn't gate MirageFallback.

♻️ Optional rename
-internal fun planRenders(stages: List<Stage>, dialect: Dialect): Boolean = stages.any { stage ->
+internal fun pipelineRenders(stages: List<Stage>, dialect: Dialect): Boolean = stages.any { stage ->

Also update the call site in MirageModifier.kt:

-    if (!planRenders(stages, currentDialect())) {
+    if (!pipelineRenders(stages, currentDialect())) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt`
around lines 120 - 129, Rename the function planRenders to pipelineRenders in
MirageProgramCache.kt and update its call site in MirageModifier.kt to use the
new name. Preserve the existing Stage dispatch and rendering behavior unchanged.
cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt (1)

159-172: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unclosed Skia Image/Bitmap per render call.

scene.render() returns a native-backed Image that's never closed, and the Bitmap() allocated in imageBytes is never closed either — only the outer ImageComposeScene is wrapped in .use. Skia bindings (Skija/Skiko) manage native memory through AutoCloseable-style disposal; leaving these unclosed leaks native buffers on every renderScene() call (6+ per test run here).

♻️ Proposed fix to close Image/Bitmap
 private fun renderScene(content: `@Composable` () -> Unit): ByteArray =
   ImageComposeScene(width = SURFACE, height = SURFACE, density = Density(1f), content = content)
     .use { scene ->
       scene.render()
-      imageBytes(scene.render())
+      scene.render().use { image -> imageBytes(image) }
     }

 private fun imageBytes(image: Image): ByteArray {
   val info = ImageInfo(SURFACE, SURFACE, ColorType.RGBA_8888, ColorAlphaType.PREMUL)
-  val bitmap = Bitmap().apply { allocPixels(info) }
-  require(image.readPixels(bitmap)) { "Image.readPixels returned false" }
-  return bitmap.readPixels() ?: error("Bitmap.readPixels returned null")
+  Bitmap().apply { allocPixels(info) }.use { bitmap ->
+    require(image.readPixels(bitmap)) { "Image.readPixels returned false" }
+    return bitmap.readPixels() ?: error("Bitmap.readPixels returned null")
+  }
 }

Please confirm org.jetbrains.skia.Image and Bitmap implement AutoCloseable/use in the Skiko version this project targets before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt`
around lines 159 - 172, Update renderScene and imageBytes to dispose every
native-backed Skia resource: wrap the Image returned by scene.render() in use
before passing it to imageBytes, and wrap the allocated Bitmap in imageBytes
with use while preserving the existing pixel-read validation and byte
conversion. Confirm the targeted Skiko APIs support AutoCloseable/use before
applying.
cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt (1)

153-160: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed Thread.sleep(600) risks CI flakiness — poll for a stable capture instead.

A hard-coded 600ms wait for the async rasterized backdrop snapshot is brittle: too slow on fast devices (wasted CI time across 3 tests × multiple captures) and potentially too short on a loaded/cold emulator (flaky failures). Since there's no public idling hook for the snapshot, a bounded retry loop that captures repeatedly until two consecutive captures match (or a timeout) would be more robust than a blind sleep.

🔁 Illustrative refactor — poll instead of fixed sleep
-    composeTestRule.mainClock.autoAdvance = true
-    Thread.sleep(600)
-    composeTestRule.waitForIdle()
-    val map = composeTestRule.onNodeWithTag(ROOT_TAG).captureToImage().toPixelMap()
+    composeTestRule.mainClock.autoAdvance = true
+    var previous: IntArray? = null
+    var map = composeTestRule.onNodeWithTag(ROOT_TAG).captureToImage().toPixelMap()
+    val deadline = System.currentTimeMillis() + 3_000
+    while (System.currentTimeMillis() < deadline) {
+      composeTestRule.waitForIdle()
+      val current = composeTestRule.onNodeWithTag(ROOT_TAG).captureToImage().toPixelMap()
+      val flat = IntArray(current.width * current.height) { i -> current[i % current.width, i / current.width].toArgb() }
+      if (previous != null && flat.contentEquals(previous)) { map = current; break }
+      previous = flat
+      map = current
+      Thread.sleep(50)
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt`
around lines 153 - 160, Replace the fixed Thread.sleep(600) in
SkyBackdropScreenshotTest with a bounded polling loop that repeatedly captures
the ROOT_TAG image until two consecutive pixel maps match or the timeout is
reached. Keep autoAdvance enabled and call waitForIdle between attempts, then
use the stable/latest capture for assertions while retaining a finite timeout to
prevent hangs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyForegroundBlurrer.kt`:
- Around line 294-303: Update captureSmallLayer so the original HARDWARE bitmap
is explicitly recycled after a successful copy to ARGB_8888, while retaining the
copied bitmap as the return value; ensure recycling occurs only after the copy
succeeds and preserve the existing failure behavior.
- Around line 186-243: Update the blur processing flow in the coroutine around
RenderScriptToolkit.blur and iterativeBlur to catch LinkageError alongside
Exception in both relevant catch blocks. Convert the error to CloudyState.Error,
invalidate node, and preserve CancellationException propagation so missing
native libraries degrade without crashing.

In `@cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectElement.kt`:
- Around line 73-113: Update EffectElement.equals and hashCode to include the
onStateChanged callback in element identity, or alternatively ensure EffectNode
refreshes its callback during update. Preserve callback replacement across
recompositions so update() is not skipped and the node invokes the latest
handler.

In `@cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectNode.kt`:
- Around line 136-145: Update EffectNode.update()’s overlay migration logic to
detect every sky change using previous !== sky, including null↔non-null
transitions. Before adopting the new sky, remove reblur from the previous
frameDriver when previous is non-null and add it to the new sky’s frameDriver
when sky is non-null, while preserving the isAttached guard and existing
snapshot/GLES invalidation for replacements.

In `@docs/src/wasmJsMain/kotlin/docs/screen/ApiMirageScreen.kt`:
- Around line 59-63: Update the descriptive text in ApiMirageScreen so the
shader sentence uses the grammatically correct article “a” before “shader,”
preserving the rest of the documentation unchanged.

---

Nitpick comments:
In
`@cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt`:
- Around line 153-160: Replace the fixed Thread.sleep(600) in
SkyBackdropScreenshotTest with a bounded polling loop that repeatedly captures
the ROOT_TAG image until two consecutive pixel maps match or the timeout is
reached. Keep autoAdvance enabled and call waitForIdle between attempts, then
use the stable/latest capture for assertions while retaining a finite timeout to
prevent hangs.

In
`@cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt`:
- Around line 120-129: Rename the function planRenders to pipelineRenders in
MirageProgramCache.kt and update its call site in MirageModifier.kt to use the
new name. Preserve the existing Stage dispatch and rendering behavior unchanged.

In `@cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt`:
- Around line 159-172: Update renderScene and imageBytes to dispose every
native-backed Skia resource: wrap the Image returned by scene.render() in use
before passing it to imageBytes, and wrap the allocated Bitmap in imageBytes
with use while preserving the existing pixel-read validation and byte
conversion. Confirm the targeted Skiko APIs support AutoCloseable/use before
applying.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f9bf469d-88b3-4443-836d-6d662b936c30

📥 Commits

Reviewing files that changed from the base of the PR and between e3ffd8e and 9f67d52.

⛔ Files ignored due to path filters (1)
  • cloudy/api/cloudy.klib.api is excluded by !cloudy/api/**/*.api
📒 Files selected for processing (76)
  • app/src/androidMain/kotlin/demo/shader/DropletMapImage.android.kt
  • app/src/commonMain/kotlin/demo/Route.kt
  • app/src/commonMain/kotlin/demo/screen/MenuHomeScreen.kt
  • app/src/commonMain/kotlin/demo/screen/MirageScreen.kt
  • app/src/commonMain/kotlin/demo/screen/MirageSkyScreen.kt
  • app/src/commonMain/kotlin/demo/shader/DropletMap.kt
  • app/src/commonMain/kotlin/demo/shader/RainyWindowShader.kt
  • app/src/skikoMain/kotlin/demo/shader/DropletMapImage.skiko.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/BackdropCaptureCrashRegressionTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlProgramMatchTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/GlesRoundtripBenchmark.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/Issue112RegressionTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MainPixelCopyCrashReproTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MirageBandScreenshotTest.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/SkyBackdropScreenshotTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MirageFilterChainTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/MiragePipelineModifierTest.kt
  • cloudy/src/androidHostTest/kotlin/com/skydoves/cloudy/ScreenshotTestSupport.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBackground.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/MirageModifier.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BackdropClearBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/BlurTier.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyBackdropBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/LegacyForegroundBlurrer.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageBackendProgram.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internal/MirageNode.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/ExperimentalMirage.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlass.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/LiquidGlassExperimental.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageModifier.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageParams.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShader.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/MirageShaders.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/CompiledProgram.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/Effect.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectElement.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropElement.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropSnapshot.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackend.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageColorGrade.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageCompiler.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageEffect.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageFilterChain.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageGlslEs.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageKernels.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageNode.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MiragePipeline.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MiragePreamble.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageProgramCache.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageUniformBinding.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/PostProcess.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageCompilerTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageElementEqualityTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MirageGlslEsTest.kt
  • cloudy/src/commonTest/kotlin/com/skydoves/cloudy/MiragePresetTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageChromaticRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageColorGradeRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/MirageProgramCacheTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/RainyWindowRasterTest.kt
  • cloudy/src/desktopTest/kotlin/com/skydoves/cloudy/SkyBackdropRasterTest.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/Cloudy.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/CloudyBackground.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/MirageModifier.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/BlurStrategy.skiko.kt
  • cloudy/src/skikoMain/kotlin/com/skydoves/cloudy/internal/MirageNode.skiko.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/ApiMirageScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/GettingStartedScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/HomeScreen.kt
  • docs/src/wasmJsMain/kotlin/docs/screen/PlaygroundScreen.kt
💤 Files with no reviewable changes (7)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropElement.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageBackdropNode.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/MirageNode.kt
  • cloudy/src/androidDeviceTest/kotlin/com/skydoves/cloudy/MainPixelCopyCrashReproTest.kt

Comment thread cloudy/src/commonMain/kotlin/com/skydoves/cloudy/internal/EffectNode.kt Outdated
Comment thread docs/src/wasmJsMain/kotlin/docs/screen/ApiMirageScreen.kt
@l2hyunwoo l2hyunwoo self-assigned this Jul 12, 2026
@l2hyunwoo
l2hyunwoo force-pushed the refactor/effect-node-pipeline branch from 9f67d52 to 4700c1b Compare July 12, 2026 10:43
l2hyunwoo added 15 commits July 12, 2026 19:47
…ics terms and collapse the source type to a nullable Sky
…kdrop snapshot inline to fix scroll staleness
github-actions Bot added a commit that referenced this pull request Jul 12, 2026
@l2hyunwoo
l2hyunwoo force-pushed the refactor/effect-node-pipeline branch from 4700c1b to 82b920f Compare July 12, 2026 10:48
github-actions Bot added a commit that referenced this pull request Jul 12, 2026
@l2hyunwoo
l2hyunwoo merged commit cb34732 into main Jul 12, 2026
7 checks passed
@l2hyunwoo
l2hyunwoo deleted the refactor/effect-node-pipeline branch July 12, 2026 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant