Skip to content

feat(package): Inject capture calls at source lines with Harmony#1683

Merged
hatayama merged 8 commits into
v3-betafrom
feat/harmony-pause-point-patcher
Jul 11, 2026
Merged

feat(package): Inject capture calls at source lines with Harmony#1683
hatayama merged 8 commits into
v3-betafrom
feat/harmony-pause-point-patcher

Conversation

@hatayama

@hatayama hatayama commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds the Harmony IL patcher that injects a captured-variable snapshot at a resolved source line, no source edit or recompile required.
  • Multiple pause points can share a method independently, be re-enabled idempotently, and be removed (restoring the method's original behavior) without a domain reload.

User Impact

  • Building on the resolver/capture work from PR 2/PR 3, this PR adds the actual runtime instrumentation: once wired up in a later PR, enable-pause-point --file <path> --line <N> will be able to pause execution and report local variables, parameters, and instance fields at that exact line.
  • This PR's new types are not yet called from any tool (wiring lands in PR 5), so there is no user-visible behavior change yet.

Changes

  • SourcePausePointPatcher: a dedicated Harmony instance resolves a method by assembly name + metadata token, checks it is patchable, and injects a call to SourcePausePointCapture.Capture at the resolved instruction index via a transpiler. A per-method injection ledger supports multiple pause points per method, idempotent re-patching of an already-patched id, and Unpatch/UnpatchAll.
  • The injected call reaches the internal Capture method across assembly boundaries because Harmony/MonoMod builds the replacement method as a skip-visibility DynamicMethod, so no InternalsVisibleTo widening beyond the test assemblies was needed.
  • Value-type this is dereferenced and boxed explicitly (ldobj+box), since ldarg.0 yields a managed pointer for struct instance methods.
  • Displaced branch-target labels and exception-region blocks are moved onto the new first instruction of an injected sequence, so loops and try/finally regions keep executing correctly after patching.
  • Unpatchable methods (abstract, extern/internal-call, open generic, [BurstCompile]) are rejected up front with a SourcePausePointPatchFailureReason and a hint pointing at the existing hand-written marker fallback.
  • Adds IsValueType to the PR 2 resolver DTOs (SourcePausePointParameter/SourcePausePointLocalVariable), needed to decide whether a captured value must be boxed.

Verification

  • dist/darwin-arm64/uloop compile — 0 errors / 0 warnings
  • dist/darwin-arm64/uloop run-tests (new SourcePausePointPatcherTests, 14/14) — instance/static/struct/loop/try-finally injection, multi-pause-point-per-method, idempotent re-patch, unpatch-then-repatch, and all four patchability-check gates
  • dist/darwin-arm64/uloop run-tests (full Editor suite) — 1849 tests, 0 failures

Review in cubic

hatayama added 4 commits July 11, 2026 09:17
The Harmony transpiler patcher (PR 4) needs to know whether each
captured parameter/local is a value type before boxing it into the
object[] array passed to Capture; boxing a reference-typed value would
be incorrect. Mono.Cecil's TypeReference.IsValueType is already
resolved from the ELEMENT_TYPE encoding without needing
TypeReference.Resolve() (which throws for external assemblies in the
Unity Editor, per the PR 2 generic ref struct fix), so it is available
at zero extra cost.
Implements SourcePausePointPatcher, injecting a call to
SourcePausePointCapture.Capture at a resolved instruction index via a
Harmony transpiler and tracking patches per method so multiple pause
points can share one method and be added/removed independently.

The call site relies on Harmony/MonoMod's skip-visibility DynamicMethod
to reach the internal Capture method across assembly boundaries, so no
InternalsVisibleTo widening was needed beyond the three test assemblies.
Value-type `this` is dereferenced and boxed explicitly (ldobj+box) since
ldarg.0 yields a managed pointer for struct instance methods. Displaced
branch-target labels and exception-region blocks are moved onto the new
first instruction of an injected sequence so loops and try/finally
regions keep executing correctly after patching.

Covers five method shapes end-to-end (instance, static, struct instance,
loop, try/finally) proving the resolved IL argument/local indices line
up against the real compiled assemblies.
Adds coverage for the parts of SourcePausePointPatcher's ledger that
weren't exercised by the single-injection acceptance tests: two pause
points landing in the same method independently, re-patching an
already-patched id as a documented no-op, and Unpatch restoring a
method's original (uninstrumented) behavior before a later re-patch
re-injects the call site.
Adds coverage for SourcePausePointPatcher.CheckPatchable's four gates:
abstract methods, extern/internal-call methods (no IL body), methods
with an unbound generic parameter of their own or declared inside an
open generic type, and methods marked (or declared inside a type
marked) [BurstCompile]. Each case is confirmed to fail before Harmony
is ever invoked and to carry the manual-marker fallback hint.

A test-only shadow of Unity.Burst.BurstCompileAttribute is defined
locally since this project does not depend on com.unity.burst;
SourcePausePointPatcher only ever compares the attribute's FullName.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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: cb2b1677-3209-44e8-bb59-6d0ccc796b27

📥 Commits

Reviewing files that changed from the base of the PR and between 1806b26 and 25faa22.

⛔ Files ignored due to path filters (1)
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherRefStructInstanceMethodFixture.cs.meta is excluded by none and included by none
📒 Files selected for processing (5)
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherRefStructInstanceMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchResult.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs
✅ Files skipped from review due to trivial changes (2)
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherRefStructInstanceMethodFixture.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchResult.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs

📝 Walkthrough

Walkthrough

Adds Harmony-based source pause point patching, value-type metadata, structured patch results, IL capture injection, patch lifecycle management, and end-to-end tests for method shapes, control flow, and unpatchable methods.

Changes

Source pause point patching

Layer / File(s) Summary
Patch contracts and value metadata
Packages/src/Editor/FirstPartyTools/PausePoint/*, Packages/src/Runtime/PausePoints/AssemblyInfo.cs
Adds patch result and failure contracts, injection metadata, value-type flags for locals and parameters, MVID propagation, constants, and test assembly access.
Harmony patch execution
Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs
Resolves methods, validates patchability and assembly freshness, injects guarded capture IL while preserving control-flow metadata, and manages patch/unpatch lifecycles.
End-to-end patch validation
Assets/Tests/Editor/SourcePausePointPatcher/*
Tests capture behavior for instance, static, struct, ref-struct, loop, and try/finally methods, multiple injections, lifecycle operations, and patchability failures.
Resolver value-type assertions
Assets/Tests/Editor/SourcePausePointResolver/*, Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointResolver.cs
Propagates value-type metadata and MVIDs, then validates value-type reporting for value and reference locals and parameters.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SourcePausePointPatcher
  participant Harmony
  participant PatchedMethod
  participant UloopPausePointRegistry
  participant SourcePausePointCapture
  SourcePausePointPatcher->>SourcePausePointPatcher: Resolve and validate method
  SourcePausePointPatcher->>Harmony: Register injection and transpiler
  Harmony->>PatchedMethod: Insert capture IL
  PatchedMethod->>UloopPausePointRegistry: Check armed state
  PatchedMethod->>SourcePausePointCapture: Capture id, instance, parameters, and locals
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: injecting capture calls at source lines with Harmony.
Description check ✅ Passed The description matches the changeset and accurately describes the new Harmony patcher and supporting resolver/test work.
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 feat/harmony-pause-point-patcher

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.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 11, 2026

@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: 2

🤖 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 `@Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs`:
- Around line 63-80: Make the ledger and Harmony updates transactional in the
patch-registration logic around InjectionsByMethod and MethodById: snapshot the
prior dictionary entries and patch state before mutation, perform Unpatch/Patch,
and restore the previous ledger entries and Harmony patch state if either
operation throws, then rethrow the original exception. Apply the same rollback
handling to the related logic at the referenced second location, and do not
swallow unexpected exceptions.
- Around line 162-168: Update CheckPatchable to reject methods whose declaring
type is byref-like before AppendInstanceLoad can emit box, returning a dedicated
SourcePausePointPatchFailureReason and the manual-marker fallback hint; add a
fixture test covering a ref struct declaring type and asserting this failure.
🪄 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: 3f64efe6-0094-4fb8-b210-43f0dab8e3a4

📥 Commits

Reviewing files that changed from the base of the PR and between 3818f79 and 31386e6.

⛔ Files ignored due to path filters (15)
  • Assets/Tests/Editor/SourcePausePointPatcher.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherLoopMethodFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherNormalMethodFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherStaticMethodFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherStructInstanceMethodFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherTryFinallyMethodFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/UnityCLILoop.Tests.Editor.SourcePausePointPatcher.asmdef is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointPatcher/UnityCLILoop.Tests.Editor.SourcePausePointPatcher.asmdef.meta is excluded by none and included by none
  • Assets/Tests/Editor/SourcePausePointResolver/Fixtures/ReferenceTypeLocalFixture.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchFailureReason.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchInjection.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchResult.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs.meta is excluded by none and included by none
📒 Files selected for processing (18)
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherLoopMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherNormalMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherStaticMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherStructInstanceMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/Fixtures/PatcherTryFinallyMethodFixture.cs
  • Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs
  • Assets/Tests/Editor/SourcePausePointResolver/Fixtures/ReferenceTypeLocalFixture.cs
  • Assets/Tests/Editor/SourcePausePointResolver/SourcePausePointResolverTests.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/AssemblyInfo.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointLocalVariable.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointParameter.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchFailureReason.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchInjection.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatchResult.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs
  • Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointResolver.cs
  • Packages/src/Runtime/PausePoints/AssemblyInfo.cs

Comment thread Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs Outdated
Comment on lines +162 to +168
if (HasBurstCompileAttribute(method) || HasBurstCompileAttribute(method.DeclaringType))
{
return SourcePausePointPatchResult.Failure(
SourcePausePointPatchFailureReason.UnpatchableBurstCompiled,
$"'{method}' (or its declaring type) is marked [BurstCompile] and cannot be patched.",
SourcePausePointConstants.ManualMarkerFallbackHint);
}

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm whether byref-like patchability and coverage already exist elsewhere.
rg -n -C4 \
  '\bref\s+struct\b|IsByRefLike(Attribute)?|Unpatchable.*(Ref|ByRef)' \
  Packages/src/Editor/FirstPartyTools/PausePoint \
  Assets/Tests/Editor/SourcePausePointPatcher

Repository: hatayama/unity-cli-loop

Length of output: 3621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the patcher and any related tests/guards for byref-like or ref-struct handling.
printf '\n== SourcePausePointPatcher outline ==\n'
ast-grep outline Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs --view expanded

printf '\n== Relevant patcher lines ==\n'
nl -ba Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs | sed -n '1,260p'

printf '\n== PausePoint tests/files mentioning byref-like or ref struct ==\n'
rg -n -C3 '\bref\s+struct\b|IsByRefLike|ManualMarkerFallbackHint|UnpatchableBurstCompiled|AppendInstanceLoad|CheckPatchable' \
  Packages/src/Editor/FirstPartyTools/PausePoint \
  Assets/Tests/Editor/SourcePausePointPatcher

Repository: hatayama/unity-cli-loop

Length of output: 1732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SourcePausePointPatcher relevant body ==\n'
sed -n '120,270p' Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs

printf '\n== Test coverage search ==\n'
rg -n -C4 '\bref\s+struct\b|IsByRefLike|ManualMarkerFallbackHint|UnpatchableBurstCompiled|AppendInstanceLoad|CheckPatchable' \
  Assets/Tests/Editor/SourcePausePointPatcher \
  Packages/src/Editor/FirstPartyTools/PausePoint

Repository: hatayama/unity-cli-loop

Length of output: 25242


Reject byref-like declaring types before emitting box. CheckPatchable still lets ref struct declaring types reach AppendInstanceLoad, which boxes value-type this and can emit invalid IL instead of the manual-marker fallback. Add a byref-like guard, a dedicated failure reason, and a fixture test.

🤖 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 `@Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs`
around lines 162 - 168, Update CheckPatchable to reject methods whose declaring
type is byref-like before AppendInstanceLoad can emit box, returning a dedicated
SourcePausePointPatchFailureReason and the manual-marker fallback hint; add a
fixture test covering a ref struct declaring type and asserting this failure.

hatayama added 4 commits July 11, 2026 10:11
Fable 5's PR review flagged that the injected transpiler prologue built
and boxed the parameter/local array unconditionally on every call, only
relying on SourcePausePointCapture's own IsArmed check afterward. Since
that check ran after the allocation-heavy IL had already executed, an
inactive (not-yet-armed) pause point still paid the cost on every hit.
Move the IsArmed check into the injected IL itself, ahead of the array
build, with a new branch label that skips straight to the original
instruction when not armed.
Fable 5's PR review flagged that TryResolveMethod trusted a resolved
metadata token against whatever assembly currently matched by name, with
no check that it was still the same compiled assembly the resolution
was taken from. After a recompile/domain reload, a stale token could
resolve to a different method than intended. Add an Mvid field to
SourcePausePointResolution (populated from Cecil's ModuleDefinition.Mvid)
and compare it against the loaded assembly's ModuleVersionId before
resolving, failing with a new StaleAssembly reason when they diverge.

Also addresses the review's should-items: switch SourcePausePointPatcher's
asserts to UnityEngine.Debug.Assert to match the rest of the PausePoint
files, add a contract check that local slot order lines up with the
resolved slot index, give AssemblyNotLoaded its own hint text instead of
reusing the manual-marker fallback message, and cover the try/finally
exception-region end boundary with a new test.
If Harmony's Patch/Unpatch call throws mid-call (e.g. building invalid
IL for a byref-like `this`), the in-memory ledger (InjectionsByMethod,
MethodById) was already updated to claim success before the Harmony
call ran. A later caller would then see a false "already patched"
state and silently miss the pause point, or Unpatch would leave other
ids on a method with no transpiler actually attached.

Wrap both call sites in try-finally (not try-catch, per this repo's
Fail Fast policy) so the ledger only commits when Harmony's call
actually completes; on failure the finally block rolls the ledger back
to its honest pre-call shape while letting the original exception
propagate unchanged.
Boxing a ref struct's `this` is illegal IL, so patching an instance
method declared on a ref struct would otherwise crash at Harmony's JIT
time. Detect byref-like declaring types (same reflection-based
IsByRefLikeAttribute check style as the existing Burst check) and emit
a null instance load instead of ldarg.0/ldobj/box, so locals and
parameters are still captured even though the instance's own fields
cannot be.

Add a Warning field to SourcePausePointPatchResult (via a default
parameter on the existing SuccessResult, keeping this repo's no-
overloads rule) so callers can surface that the instance was not
captured. Add a ref-struct fixture and test covering the degraded
capture path.
@hatayama

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@hatayama

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@hatayama hatayama dismissed coderabbitai[bot]’s stale review July 11, 2026 01:58

Both findings were addressed in ef0f1ce (try-finally ledger compensation) and 25faa22 (byref-like this degrades to null instance with Warning), arbitrated and re-reviewed (LGTM). CodeRabbit incremental review could not re-review due to rate limiting; dismissed with user approval.

@hatayama hatayama merged commit 0159711 into v3-beta Jul 11, 2026
10 checks passed
@hatayama hatayama deleted the feat/harmony-pause-point-patcher branch July 11, 2026 01:59
@github-actions github-actions Bot mentioned this pull request Jul 11, 2026
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