Skip to content

refactor!: read generated resource defaults from declaration state - #2210

Open
yuto-trd wants to merge 1 commit into
yuto-trd/detached-resource-originalfrom
yuto-trd/resource-defaults-provider
Open

refactor!: read generated resource defaults from declaration state#2210
yuto-trd wants to merge 1 commit into
yuto-trd/detached-resource-originalfrom
yuto-trd/resource-defaults-provider

Conversation

@yuto-trd

Copy link
Copy Markdown
Member

Description

Stacked on #2205. Review that one first; this PR's diff against main includes it.

A detached generated Resource started every value property at default(T) regardless of what the owner declared. A resource built through its public constructor therefore disagreed with the same owner's declared defaults — a SolidColorBrush.Resource built directly did not carry the Opacity its IProperty declares.

The generator also had an unstated requirement. To read those defaults it needs the owner's IProperty instances to be available without running a user constructor. Nothing checked that, so a type whose properties were built in a constructor silently produced wrong defaults rather than failing.

This change does two things:

Generated resources chain through the declared defaults. A concrete generated Resource() reads them from __CreateResourceDefaultValues(). The attached ToResource path uses a separate constructor that skips evaluating defaults it would immediately overwrite. EngineObject.Resource gains the matching protected constructors.

Four diagnostics make the implicit requirement checkable.

ID Reports
BESG003 An IProperty not available from declaration-time state, or replaced in a constructor
BESG004 A primary constructor on the automatic defaults path
BESG005 An invalid or ambiguous [ResourceDefaultValuesProvider]
BESG006 A derived type inheriting a provider instead of declaring its own

[ResourceDefaultValuesProvider] is the escape hatch for the first two: a static parameterless non-generic method returning the declaring owner. It is what a primary-constructor type uses to keep generation.

BESG004 already fires on in-tree code — FaultingDrawable in RendererExceptionSafetyTests used a primary constructor — which is migrated to an ordinary constructor here.

Affected areas

  • Beutl.Engine (rendering / scene / track)
  • Beutl.ProjectSystem (project / document persistence)
  • UI (Beutl.Editor, Beutl.Editor.Components, Beutl.Controls)
  • Beutl.Extensibility (plugin abstractions)
  • Beutl.NodeGraph (node editor)
  • Beutl.FFmpegIpc / Beutl.FFmpegWorker (media IPC boundary)
  • Beutl.Api (server API client)
  • Build / CI / docs only

Also touches Beutl.Engine.SourceGenerators.

Breaking changes

Declaration-time defaults are required. A resource-generating EngineObject subclass without an explicit provider must expose every generated IProperty from stable declaration-time state: an auto-property with a declaration initializer, or a computed getter returning a declaration-initialized readonly field, with no ordinary constructor replacing that storage.

Migration: move property creation to a supported declaration shape, move primary-constructor logic to an ordinary constructor, or declare one [ResourceDefaultValuesProvider] static parameterless non-generic method returning the declaring owner. Suppressing generation and implementing Resource/ToResource manually remains the alternative.

Generated abstract Resource types no longer expose a protected parameterless constructor. A hand-written or generation-suppressed attached resource chains to base(skipDefaultInitialization: true) before its first Update; a hand-written detached resource that promises declared-default parity chains to base(defaultValues). This replaces the implicit base() path that left abstract-base properties at default(T).

The attached-only ParticleEmitter.Resource, ShakeEffect.Resource, DelayAnimationEffect.Resource, NodeGraphDrawable.Resource, NodeGraphFilterEffect.Resource, and RenderNodeDrawable.Resource parameterless constructors are now internal. Callers construct the owner and call ToResource(CompositionContext) rather than exposing a pre-Update resource.

Test plan

  • Added tests/SourceGeneratorTest/ResourceDefaultValuesTests.cs (7 tests): the generated constructor chain and per-property default seeding, the attached fast path, one scenario per diagnostic (BESG003BESG006), and a valid-provider case proving the escape hatch keeps generation working for a primary-constructor type.
  • dotnet build Beutl.slnx — 0 warnings, 0 errors.
  • dotnet test tests/Beutl.UnitTests — 4978 passed, 0 failed, 3 skipped.
  • dotnet test tests/SourceGeneratorTest — 18 passed, 0 failed.

Fixed issues / References

Split out of the feature 004 (GPU pass fusion) stack. The generator changes reference no feature-004 type, so this stands on its own; the ownership-transfer seam that shipped alongside it in that branch is deliberately not included here.

…state

A detached generated Resource started every value property at default(T)
regardless of what the owner declared, so a resource built through its public
constructor disagreed with the same owner's declared defaults. Nothing made the
generator's assumption explicit either: it needed the owner's IProperty
instances to be readable without running a user constructor, but silently
produced wrong defaults when they were not.

Generated resources now chain through the owner's declared defaults. A concrete
generated Resource() reads them from __CreateResourceDefaultValues(), the
attached ToResource path uses a separate constructor that skips default
evaluation it would immediately overwrite, and EngineObject.Resource exposes the
matching protected constructors.

Four diagnostics make the previously implicit requirement checkable. BESG003
reports a property whose IProperty is not available from declaration-time state
or is replaced in a constructor, and BESG004 reports a primary constructor on
that automatic path. [ResourceDefaultValuesProvider] is the escape hatch for
both: BESG005 rejects an invalid or ambiguous provider, and BESG006 requires a
derived type to declare its own rather than inherit one that would evaluate the
base owner's defaults.

BESG004 already fires on in-tree code: FaultingDrawable in
RendererExceptionSafetyTests used a primary constructor, and is migrated to an
ordinary one here.

BREAKING CHANGE: A resource-generating EngineObject subclass without an explicit
defaults provider must expose every generated IProperty from stable
declaration-time state - an auto-property with a declaration initializer, or a
computed getter returning a declaration-initialized readonly field - and no
ordinary constructor may replace that storage. BESG003 reports unsupported or
constructor-replaced storage and BESG004 reports a primary constructor on this
path. Authors move property creation to a supported declaration shape, move
primary-constructor logic to an ordinary constructor, or declare exactly one
[ResourceDefaultValuesProvider] static parameterless non-generic method
returning the declaring owner; BESG005 rejects an invalid or ambiguous provider
and BESG006 requires each generated derived type in a provider-backed hierarchy
to declare its own. Suppressing generation and implementing the Resource /
ToResource contract manually remains the alternative.

BREAKING CHANGE: Generated abstract Resource types no longer expose a protected
parameterless constructor. A hand-written or generation-suppressed attached
resource must chain to base(skipDefaultInitialization: true) before its first
Update; a hand-written detached resource that promises declared-default parity
chains to base(defaultValues). This replaces the implicit base() path that left
abstract-base properties at default(T). The attached-only ParticleEmitter,
ShakeEffect, DelayAnimationEffect, NodeGraphDrawable, NodeGraphFilterEffect, and
RenderNodeDrawable Resource parameterless constructors are now internal; callers
construct the owner and call ToResource(CompositionContext) instead of exposing
a pre-Update resource. Affects Beutl.Engine and Beutl.NodeGraph.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0b1a527-d1d9-4a43-ab57-de3f3d598857

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions

Copy link
Copy Markdown
Contributor

No TODO comments were found.

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defects identified.

The generated detached and attached constructor paths remain distinct, inherited resource defaults flow through the generated base chain, and the affected handwritten resources explicitly adopt the new attached-only constructor contract.

Important Files Changed

Filename Overview
src/Beutl.Engine.SourceGenerators/EngineObjectResourceGenerator.cs Adds validation for declaration-time property storage, primary constructors, and explicit default providers before generating resource members.
src/Beutl.Engine.SourceGenerators/Emit/ResourceDefaultValuesEmitter.cs Emits the initializer-only owner construction path or delegates default-owner creation to a validated provider.
src/Beutl.Engine.SourceGenerators/Emit/ResourceClassEmitter.cs Adds detached and attached resource constructors and seeds generated fields from declared defaults.
src/Beutl.Engine.SourceGenerators/Emit/ToResourceMethodEmitter.cs Routes attached resource creation through the skip-default-initialization factory before Update populates it.
src/Beutl.Engine/Engine/EngineObject.cs Adds matching owner and base-resource constructor contracts for generated detached and attached paths.
src/Beutl.Engine/Engine/ResourceDefaultValuesProviderAttribute.cs Introduces the explicit factory extension point for owners whose defaults require constructor logic.
tests/SourceGeneratorTest/ResourceDefaultValuesTests.cs Covers constructor emission, attached creation, provider behavior, and all four new diagnostics.
src/Beutl.NodeGraph/NodeGraphFilterEffect.cs Migrates the handwritten attached-only Resource constructor to the explicit skip-default-initialization base path.

Reviews (1): Last reviewed commit: "refactor(engine)!: read generated resour..." | Re-trigger Greptile

@drift-check

drift-check Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review Bot

No comment/code divergences or documentation drift detected. Reviewed 21 file(s); skipped 0.

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