Skip to content

user variables for scripted cycles#110

Draft
unlessgames wants to merge 8 commits intodevfrom
cycle-user-vars
Draft

user variables for scripted cycles#110
unlessgames wants to merge 8 commits intodevfrom
cycle-user-vars

Conversation

@unlessgames
Copy link
Copy Markdown
Collaborator

@unlessgames unlessgames commented Mar 6, 2026

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
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

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
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

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
Copy link
Copy Markdown

github-actions Bot commented Mar 6, 2026

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
Copy link
Copy Markdown
Member

emuell commented Mar 6, 2026

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
Copy link
Copy Markdown
Member

emuell commented Apr 1, 2026

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
Copy link
Copy Markdown
Member

emuell commented Apr 18, 2026

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
Copy link
Copy Markdown
Member

emuell commented Apr 20, 2026

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
Copy link
Copy Markdown
Member

emuell commented Apr 21, 2026

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
Copy link
Copy Markdown
Member

emuell commented Apr 21, 2026

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%

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