Skip to content

user variables for scripted cycles - #110

Merged
emuell merged 10 commits into
devfrom
cycle-user-vars
Apr 24, 2026
Merged

user variables for scripted cycles#110
emuell merged 10 commits into
devfrom
cycle-user-vars

Conversation

@unlessgames

@unlessgames unlessgames commented Mar 6, 2026

Copy link
Copy Markdown
Collaborator

An initial draft for the discussed vars function on the cycle object.

Works similar to :map in that you can supply either a static table or a function returning a table.

I guess we'll need a different context for vars? Since variables are only being assigned once per generate instead of for each event, the fields related to single steps are meaningless.

Here, the table variant inserts the variables to the cycle directly instead of passing around the table (contrary to the case with map) whereas the callback variant follows a similar pattern to the mapping_function.

Not sure what else should happen with the callback and its errors, but it seems to work fine so far.

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

Benchmark for f153fdf

Click to view benchmark
Test Base PR %
Cycle/Generate 52.6±0.71µs 52.4±0.65µs -0.38%
Cycle/Parse 308.2±6.75µs 318.7±5.74µs +3.41%
Rust Phrase/Clone 434.8±5.84ns 420.0±6.61ns -3.40%
Rust Phrase/Create 65.0±1.03µs 67.0±1.62µs +3.08%
Rust Phrase/Run 619.6±5.92µs 626.4±8.75µs +1.10%
Rust Phrase/Seek 127.3±231.54µs 139.1±255.27µs +9.27%
Scripted Phrase/Clone 631.3±10.45ns 623.7±7.86ns -1.20%
Scripted Phrase/Create 1025.9±38.01µs 995.3±18.79µs -2.98%
Scripted Phrase/Run 1657.7±11.40µs 1641.7±16.49µs -0.97%
Scripted Phrase/Seek 214.9±435.32µs 205.1±408.79µs -4.56%

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

Benchmark for c8242c9

Click to view benchmark
Test Base PR %
Cycle/Generate 52.9±0.53µs 52.3±0.34µs -1.13%
Cycle/Parse 316.3±4.35µs 311.9±4.88µs -1.39%
Rust Phrase/Clone 429.8±8.69ns 425.6±12.07ns -0.98%
Rust Phrase/Create 67.0±1.30µs 65.4±1.19µs -2.39%
Rust Phrase/Run 627.1±6.42µs 645.6±4.86µs +2.95%
Rust Phrase/Seek 132.5±242.42µs 135.7±249.90µs +2.42%
Scripted Phrase/Clone 635.6±11.68ns 631.6±5.54ns -0.63%
Scripted Phrase/Create 1017.4±33.02µs 1003.0±34.54µs -1.42%
Scripted Phrase/Run 1657.1±18.77µs 1616.5±20.19µs -2.45%
Scripted Phrase/Seek 215.0±434.49µs 203.8±407.01µs -5.21%

@github-actions

github-actions Bot commented Mar 6, 2026

Copy link
Copy Markdown

Benchmark for 77adad2

Click to view benchmark
Test Base PR %
Cycle/Generate 52.0±0.34µs 52.5±1.54µs +0.96%
Cycle/Parse 315.8±6.34µs 309.2±4.24µs -2.09%
Rust Phrase/Clone 431.8±7.61ns 426.8±6.61ns -1.16%
Rust Phrase/Create 66.6±1.74µs 65.7±2.05µs -1.35%
Rust Phrase/Run 639.5±14.46µs 645.6±6.08µs +0.95%
Rust Phrase/Seek 127.7±231.26µs 136.8±253.06µs +7.13%
Scripted Phrase/Clone 637.1±6.87ns 637.9±11.71ns +0.13%
Scripted Phrase/Create 994.8±18.35µs 1011.6±67.51µs +1.69%
Scripted Phrase/Run 1665.4±11.42µs 1637.2±10.42µs -1.69%
Scripted Phrase/Seek 216.2±437.92µs 214.0±433.11µs -1.02%

@emuell

emuell commented Mar 6, 2026

Copy link
Copy Markdown
Member

Looks great. I'll add a few nitpicking comments in the review and will do more testing over the weekend. Else this looks straight forward and like a really great addition!

@unlessgames

Copy link
Copy Markdown
Collaborator Author

Hey, did you mean that you wanted to add those comments to this as-is, or are you waiting for me here to make the PR more complete? I'd appreciate any nitpicks here before continuing!

@emuell

emuell commented Apr 1, 2026

Copy link
Copy Markdown
Member

There should be a new with_variables_callback instead of adding this into the existing off-topic with_mapping_callback. I can fix that too if you want me to.

@github-actions

Copy link
Copy Markdown

Benchmark for cf34af4

Click to view benchmark
Test Base PR %
Cycle/Generate 52.0±0.73µs 52.0±0.56µs 0.00%
Cycle/Parse 317.9±4.22µs 317.3±2.86µs -0.19%
Rust Phrase/Clone 432.1±5.38ns 443.2±7.19ns +2.57%
Rust Phrase/Create 67.9±0.66µs 67.9±1.38µs 0.00%
Rust Phrase/Run 633.3±4.48µs 628.4±5.33µs -0.77%
Rust Phrase/Seek 135.0±248.78µs 136.2±251.75µs +0.89%
Scripted Phrase/Clone 661.7±6.72ns 619.5±12.00ns -6.38%
Scripted Phrase/Create 1012.0±25.84µs 1006.0±14.24µs -0.59%
Scripted Phrase/Run 1670.2±9.19µs 1621.8±10.62µs -2.90%
Scripted Phrase/Seek 220.5±446.09µs 214.9±435.42µs -2.54%

@unlessgames

Copy link
Copy Markdown
Collaborator Author

I've separated those and refactored the scripted cycle a bit, since the cycle either has a static table mapping or a callback (and a timeout hook always exist with a callback and never with a mapping), having these as an enum makes the code more self-explanatory.

Added a new kind of context for the variables callback that gets the parameters and the current iteration of the cycle assigned, do you think anything else would make sense here?

Updated the API docs and the guide with the var function example.

@github-actions

Copy link
Copy Markdown

Benchmark for 259018a

Click to view benchmark
Test Base PR %
Cycle/Generate 53.6±0.49µs 51.0±0.80µs -4.85%
Cycle/Parse 318.5±3.51µs 329.3±5.87µs +3.39%
Rust Phrase/Clone 448.5±6.45ns 423.6±6.52ns -5.55%
Rust Phrase/Create 67.8±1.43µs 69.6±1.25µs +2.65%
Rust Phrase/Run 637.7±5.38µs 627.5±5.79µs -1.60%
Rust Phrase/Seek 135.2±249.06µs 136.2±251.36µs +0.74%
Scripted Phrase/Clone 662.0±11.34ns 620.4±17.39ns -6.28%
Scripted Phrase/Create 1011.0±23.96µs 1003.7±26.08µs -0.72%
Scripted Phrase/Run 1676.9±12.04µs 1629.0±22.32µs -2.86%
Scripted Phrase/Seek 222.0±448.22µs 216.8±438.46µs -2.34%

@emuell

emuell commented Apr 18, 2026

Copy link
Copy Markdown
Member

That looks great. Thanks. Give me some time to review and test this, as this is a larger change. Will do do ASAP beginning next week.

@emuell

emuell commented Apr 20, 2026

Copy link
Copy Markdown
Member

do you think anything else would make sense here?

Yes, all non mapping specific stuff that's also in the mapping callbacks: playback_state, time_base

There are a few other small issues, like missing resets for the callback. This stuff unfortunately is really tricky in the details.

I'll go though this in detail tomorrow and will fix that, okay?

@unlessgames

Copy link
Copy Markdown
Collaborator Author

I'll go though this in detail tomorrow and will fix that, okay?

That would be great!

@github-actions

Copy link
Copy Markdown

Benchmark for 21d5337

Click to view benchmark
Test Base PR %
Cycle/Generate 51.0±0.69µs 51.3±0.65µs +0.59%
Cycle/Parse 316.4±6.55µs 324.9±4.55µs +2.69%
Rust Phrase/Clone 432.5±19.25ns 425.1±4.31ns -1.71%
Rust Phrase/Create 66.3±1.33µs 69.7±1.09µs +5.13%
Rust Phrase/Run 632.2±9.50µs 633.5±4.64µs +0.21%
Rust Phrase/Seek 135.5±249.02µs 134.2±247.59µs -0.96%
Scripted Phrase/Clone 657.8±12.10ns 630.8±28.11ns -4.10%
Scripted Phrase/Create 1005.1±28.14µs 1017.8±31.07µs +1.26%
Scripted Phrase/Run 1661.4±19.06µs 1612.8±21.89µs -2.93%
Scripted Phrase/Seek 218.2±441.46µs 203.7±405.48µs -6.65%

@emuell

emuell commented Apr 21, 2026

Copy link
Copy Markdown
Member

Fixed a few little things here and there and updated the callback context generation and docs:

This is what the map and var functions now get as context.

    /// Sets the cycle context for the mapping callbacks.
    pub fn set_cycle_map_context(
        &mut self,
        playback_state: ContextPlaybackState,
        time_base: &BeatTimeBase,
        channel: usize,
        step: usize,
        step_length: f64,
    ) -> LuaResult<()> {
        self.set_context_playback_state(playback_state)?;
        self.set_context_time_base(time_base)?;
        self.set_context_cycle_step(channel, step, step_length)?;
        Ok(())
    }

    /// Sets the cycle context for var callbacks.
    pub fn set_cycle_var_context(
        &mut self,
        playback_state: ContextPlaybackState,
        time_base: &BeatTimeBase,
        parameters: &ParameterSet,
        iteration: u32,
    ) -> LuaResult<()> {
        self.set_context_playback_state(playback_state)?;
        self.set_context_time_base(time_base)?;
        self.set_context_parameters(&parameters)?;
        self.set_context_cycle_iteration(iteration)?;
        Ok(())
    }

Actually wouldn't hurt to also add the new iteration to the map context.


Using an enum for the map callback or static map makes a lot of sense as they are mutually exclusive. I think we should do the same for the var internally, for the sake of consistency and also to reset the vars when parameters changed.

Unlikely to be a problem, but if both, parameter and the map map set variables in the cycle, it's not clearly defined what gets set first - what overrides what. I think it should be parameters first, then the content from the map, just like it happens with dynamic var callbacks.

I'll try to take care of this tomorrow. Else I think this is ready and works great as it is!

@unlessgames

Copy link
Copy Markdown
Collaborator Author

Thanks!

It's a small thing but this updated description of the iteration field is a bit misleading I think

how often the cycle has been run.

I've added one to the iteration (that starts from zero) so it would be an idiomatic index in lua. When the var function runs, iteration is the index of the current iteration that the function's result will apply to, but this description makes it sound like it would be -1 from that (since it is phrased it past tense), and "often" here feels more like it would mean "how frequently the cycle has been run", but the value isn't really about that, it's just a simple count (or index).


It's tangentially related but while experimenting with variables I found the way enum parameter's need to be defined to be fairly inergonomic in that you have to write one case two times (one for the default arg and one inside the list). The work around is to define the table outside but that's adding boilerplate. It would be nice if for example enum could take a nil as its default arg in which case it would just use the first entry in the list, or it could take an integer and mod it with the list's length to get a default.

@emuell

emuell commented Apr 21, 2026

Copy link
Copy Markdown
Member

It's a small thing but this updated description of the iteration field is a bit misleading I think

The old doc comment got removed by accident. Sorry. Restored it now...

It would be nice if for example enum could take a nil as its default arg in which case it would just use the first entry in the list, or it could take an integer and mod it with the list's length to get a default.

Yes, definitely. I think it would be "safe" to allow strings or numbers here. When passing a string, it must be one of the enum values, when passing a number it's interpreted as index. So using 1 here selects first value as default value. nil also wouldn't hurt, but I think 1 is fine and not too hard to type. I'll give this a try as soon as this is merged...

@github-actions

Copy link
Copy Markdown

Benchmark for fc659d9

Click to view benchmark
Test Base PR %
Cycle/Generate 52.0±0.46µs 52.4±1.39µs +0.77%
Cycle/Parse 323.2±5.14µs 316.4±3.28µs -2.10%
Rust Phrase/Clone 431.0±6.34ns 418.4±6.17ns -2.92%
Rust Phrase/Create 69.3±1.31µs 67.4±1.67µs -2.74%
Rust Phrase/Run 647.7±4.32µs 626.1±3.47µs -3.33%
Rust Phrase/Seek 133.8±247.52µs 136.4±251.89µs +1.94%
Scripted Phrase/Clone 663.1±8.82ns 632.0±10.12ns -4.69%
Scripted Phrase/Create 1022.4±26.57µs 1019.5±16.41µs -0.28%
Scripted Phrase/Run 1645.7±11.49µs 1612.3±15.07µs -2.03%
Scripted Phrase/Seek 230.5±467.40µs 203.9±407.66µs -11.54%

@emuell

emuell commented Apr 23, 2026

Copy link
Copy Markdown
Member

I've now made variables and mappings both:

mappings: ScriptedCycleMapping<Vec<Option<NoteEvent>>>,
variables: ScriptedCycleMapping<CycleSubCycle>,

in CycleUserData and ScriptedCycleEmitter

set_var now also only happens in ScriptedCycleEmitter so it's clear when they get set and in which order.

Currently, static variable tables are applied before parameter variables because the generate function applies parameter values on every run, as these values may change. If we want var to override parameter values, then we'd need to apply them every time in generate as well. Currently, this avoids some overhead by not doing so.

Tested everything as thoroughly as possible. It would be great if you could also give this a try. Seems ready to merge from my side.

@emuell
emuell marked this pull request as ready for review April 23, 2026 11:48
@unlessgames

Copy link
Copy Markdown
Collaborator Author

I've now made variables and mappings both:

mappings: ScriptedCycleMapping<Vec<Option<NoteEvent>>>,
variables: ScriptedCycleMapping<CycleSubCycle>,

in CycleUserData and ScriptedCycleEmitter

Did you forgot to push this?


I think we definitely want to let tables override the parameters. This is surely the more useful and less confusing behaviour.

If we already cache the table the way you described above, then we could simply skip assigning/converting any parameter to a variable whose identifier exists in the variables table. This would result in less overhead as we could skip parsing parameters when their results would be overriden by the table anyway.

Later on we can add caching unchanged parameter results as well.

@emuell

emuell commented Apr 23, 2026

Copy link
Copy Markdown
Member

Did you forgot to push this?

Oops. Indeed. Will do so tomorrow. Don't have that here on this computer.

I think we definitely want to let tables override the parameters.

Agree, but this will be more work then. I'll take care of this tomorrow as well.

…make clear when and how vars

are
applied within the scripted cycle
@emuell

emuell commented Apr 24, 2026

Copy link
Copy Markdown
Member

Pushed that now and also added the var, parameter override. Sorry for the fuzz.

@github-actions

Copy link
Copy Markdown

Benchmark for 5710dd1

Click to view benchmark
Test Base PR %
Cycle/Generate 53.1±0.61µs 50.6±0.66µs -4.71%
Cycle/Parse 319.8±5.46µs 312.2±5.44µs -2.38%
Rust Phrase/Clone 433.6±10.01ns 436.6±7.40ns +0.69%
Rust Phrase/Create 67.1±1.55µs 66.2±1.56µs -1.34%
Rust Phrase/Run 647.9±13.01µs 635.2±10.10µs -1.96%
Rust Phrase/Seek 135.0±248.96µs 135.4±252.39µs +0.30%
Scripted Phrase/Clone 667.2±7.61ns 619.9±6.69ns -7.09%
Scripted Phrase/Create 1026.3±66.36µs 998.3±19.22µs -2.73%
Scripted Phrase/Run 1646.4±12.24µs 1624.1±11.88µs -1.35%
Scripted Phrase/Seek 220.0±444.29µs 207.5±415.03µs -5.68%

@unlessgames unlessgames left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Looks good!

@github-actions

Copy link
Copy Markdown

Benchmark for 68dab3d

Click to view benchmark
Test Base PR %
Cycle/Generate 55.0±1.35µs 53.0±0.52µs -3.64%
Cycle/Parse 289.2±12.46µs 286.7±3.71µs -0.86%
Rust Phrase/Clone 459.7±9.57ns 438.0±6.10ns -4.72%
Rust Phrase/Create 63.2±2.05µs 62.0±0.85µs -1.90%
Rust Phrase/Run 660.4±7.00µs 659.8±3.67µs -0.09%
Rust Phrase/Seek 144.6±268.04µs 143.4±267.94µs -0.83%
Scripted Phrase/Clone 712.2±9.24ns 656.9±8.48ns -7.76%
Scripted Phrase/Create 962.5±22.77µs 968.6±15.68µs +0.63%
Scripted Phrase/Run 1710.7±20.53µs 1664.2±22.52µs -2.72%
Scripted Phrase/Seek 230.7±468.11µs 219.3±442.16µs -4.94%

@emuell
emuell merged commit 4d03378 into dev Apr 24, 2026
2 checks passed
@emuell
emuell deleted the cycle-user-vars branch April 24, 2026 20:25
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.

2 participants