Skip to content

Time offset bindings - #129

Draft
JuhDuh26 wants to merge 18 commits into
carbonengine:mainfrom
JuhDuh26:time_offset_bindings
Draft

JuhDuh26 wants to merge 18 commits into
carbonengine:mainfrom
JuhDuh26:time_offset_bindings

Conversation

@JuhDuh26

@JuhDuh26 JuhDuh26 commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

Allows designers/developers to reuse a curve (Noise over time for example) by applying a time offset to the binding. desyncronising the peaks and troughs of the curve with other bindings using the same curve. which enables much more naturalistic "random" looking patterns, or even phasing/sequencing.

2026-10-20.12-03-17.mp4
2026-10-20.15-14-17.mp4

Linked issue (optional)

https://fenriscreations.atlassian.net/browse/EF-20375

Testing

  • Create a curve
  • Bind two different properties to the curve
  • On one of the bindings change the "Time Offset" to see the binding values jump relatively ahead or behind in the curve's timeline

AI assistance

Claude wrote all the code for this feature, and I iteratively used the thermo-nuclear-code-quality-review skill to highlight and resolve any implementation issues.

I read all the rationale and outputted code. I have a lot of experience with C#/Java/Python, but not so much C++.

A lot of tests were added which may or may not be overkill depending on your view.

Checklist

  • Read CONTRIBUTING.md.
    • Commits follow the commit-message style described there.
    • Added or updated tests where it made sense.
    • Updated docs / inline API comments for any behaviour change.

JuhDuh26 and others added 18 commits October 16, 2026 17:53
A time-offset binding used to call UpdateValue on its source curve at the
offset time and then again at the curve set's time to put it back. That
re-fired every event key on a TriEventCurve each frame, dragged curves owned
by other curve sets to this set's time, and evaluated the curve twice more
per frame.

Bindings now sample through the typed curves' GetValueAt, which leaves the
curve's state alone. The binding decides this when it resolves its source:
only the currentValue of a scalar, vector, quaternion or color curve can be
sampled, and an offset on anything else is ignored with a warning.

The curve set no longer special-cases offset bindings. ITr2ValueBinding has
a single CopyValueAt(TriCurveSetTime) instead of GetTimeOffset and
CopyValueSampledAt, and TriCurveSetTime carries the looped range so the
binding can wrap its own offset. The range wrapping is shared with the curve
set's own time update, which also stops a zero-length looped range from
producing NaN.

Also drops the unused TriValueBinding::SetTimeOffset, moves the Tracy link
fix to its own branch, and adds tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fails on the previous implementation, which restored the curve to the
binding's own curve set's time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The timeOffset description claimed every ignored offset logs a warning.
  Bindings run without a curve set time (lens flares, child links, socket
  parameters, dynamic bindings) ignore it silently; say so.
- TriCurveSampleFunc writes into a Vector4& instead of a void* whose size
  was only stated in a comment.
- Test vector and color curves and a swizzled source (currentValue.y),
  which exercises the sample offset path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timing on a TrinityDev build showed one curve driving two offset bindings
costing 1.5x (4 keys) to 17x (512 keys) the time of two duplicated curves.
Two causes:

- Every sample looked up the curve's typed interface again (QueryInterface
  plus a reference count round trip). TriCurveSampler now finds it once when
  the binding resolves its source and keeps the raw pointer, which lives
  exactly as long as the binding's existing pointer into the source.
- Tr2CurveScalar caches the last key segment it used. Sampling one curve at
  two times a frame keeps moving that cache, and every miss scanned all keys.
  A miss now binary searches the sorted keys, keeping the linear scan only
  for keys edited out of order without OnKeysChanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exercises the scalar curve's key search on a cache miss.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared curve sampled at its curve set's time and at a binding's offset
time kept moving its single cached key segment, so most lookups searched.
That left one shared curve about 2.2x the cost of two duplicated curves.

Tr2CurveScalar and Tr2CurveQuaternion now look their keys up through one
FindCurveSegment helper that takes the cache to use, replacing the two
copies of the same lookup. Their own GetValue passes the curve's cache as
before; the new SampleValue takes the caller's. TriCurveSampler recognises
these keyed curves and keeps a segment cache per binding, so an offset
binding finds its keys as quickly as a duplicated curve would, and leaves
the curve's own cache alone.

The quaternion curve also gets the binary search on a cache miss, and both
can now step back from segment 1 to segment 0 from the cache (the left
neighbour check stopped at 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timing showed the shared segment lookup slowed every curve evaluation by
about 25-30 ns in unoptimised builds: GetValue, SampleValue, the lookup and
a lambda each cost a real call there. The curves' own UpdateValue, Update
and GetValueAt now call SampleValue directly, and the cache checks in
FindCurveSegment are written out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FindCurveSegment was still one extra call on every curve evaluation, which
cost about 15 ns in unoptimised builds. Curves now check their cached
segment inline and only call it on a miss.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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