@@ -15,11 +15,24 @@ typedef struct sequence_info_t {
1515 //uint32_t tag; // tag is implicit, it's its index in the table
1616 uint32_t tick ; // 0 means not used
1717 uint32_t period ; // 0 means not used
18+ // Next OCCUPIED slot, or -1 for the end. Only meaningful while this
19+ // entry has a wire -- `wire != NULL` is what "in the list" means, so
20+ // there is one source of truth and not two to keep in step.
21+ int32_t next_active ;
1822} sequence_info_t ;
1923
2024struct sequence_info_t * sequences = NULL ; // An array indexed by tag.
2125int32_t max_sequences = 0 ; // Number of user-addressable tags.
22- int32_t highest_tag = -1 ;
26+ // Head of the ascending list of occupied slots (user tags and anonymous
27+ // entries alike); -1 when nothing is scheduled. This replaces `highest_tag`,
28+ // which was a HIGH-WATER MARK: it only ever grew, so one event at a high tag
29+ // made every tick scan that far for the rest of the session, long after that
30+ // sequence was cleared. The anonymous pool made that the common case, not a
31+ // corner: anonymous entries are allocated round-robin at indices past
32+ // max_sequences, so a burst of ticks= one-shots pinned the mark at the very
33+ // end of the table permanently. The cost is proportional to what is
34+ // scheduled now.
35+ int32_t first_active = -1 ;
2336// Anonymous (no-tag) entries live past the user-addressable tag range, at
2437// indices [max_sequences .. max_sequences+AMY_ANON_SEQUENCE_SLOTS), so a
2538// user-supplied tag (bounds-checked against max_sequences) can never reach
@@ -49,7 +62,9 @@ void sequencer_init(int max_sequencer_tags) {
4962 sequences [i ].wire = NULL ;
5063 sequences [i ].tick = 0 ;
5164 sequences [i ].period = 0 ;
65+ sequences [i ].next_active = -1 ;
5266 }
67+ first_active = -1 ;
5368 // We are read to go.
5469 sequencer_recompute ();
5570}
@@ -64,8 +79,9 @@ void sequencer_reset() {
6479 sequences [i ].tick = 0 ;
6580 sequences [i ].period = 0 ;
6681 }
82+ sequences [i ].next_active = -1 ;
6783 }
68- highest_tag = -1 ;
84+ first_active = -1 ;
6985}
7086
7187void sequencer_deinit () {
@@ -78,15 +94,70 @@ void sequencer_deinit() {
7894}
7995
8096void sequencer_debug () {
81- fprintf (stderr , "sequencer: max_sequences %" PRIi32 " highest_tag %" PRIi32 "\n" , max_sequences , highest_tag );
82- for (int32_t tag = 0 ; tag <= highest_tag ; ++ tag ) {
97+ int32_t n_active = 0 ;
98+ for (int32_t t = first_active ; t != -1 ; t = sequences [t ].next_active ) ++ n_active ;
99+ fprintf (stderr , "sequencer: max_sequences %" PRIi32 " active %" PRIi32 "\n" , max_sequences , n_active );
100+ for (int32_t tag = first_active ; tag != -1 ; tag = sequences [tag ].next_active ) {
83101 if (sequences [tag ].wire ) {
84102 fprintf (stderr , "sequence tag %" PRIi32 "%s tick %" PRIu32 " period %" PRIu32 " wire \"%s\"\n" ,
85103 tag , tag >= max_sequences ? " (anon)" : "" , sequences [tag ].tick , sequences [tag ].period , sequences [tag ].wire );
86104 }
87105 }
88106}
89107
108+ /* The occupied slots, threaded through the table as an ASCENDING list.
109+ *
110+ * Why threaded rather than a list of its own: the table has to stay
111+ * indexable, because add and clear both reach a tag directly and want O(1)
112+ * to do it. This gets the tick scan down to the number of sequences
113+ * actually scheduled without giving that up, and without allocating
114+ * anything the render thread could walk into while it is being freed.
115+ *
116+ * WHY ASCENDING, and it is not tidiness: two sequences that hit on the same
117+ * tick play in the order they are visited, so the order decides which one
118+ * wins if they touch the same parameter. That order was slot order when
119+ * this was an indexed sweep, and keeping the list sorted keeps it slot
120+ * order. An insertion-ordered list would make a pattern sound different
121+ * after an edit.
122+ *
123+ * THREAD SAFETY. Link mutations happen only under the amy lock --
124+ * sequencer_add_wire() takes it, the tick loop's delete path takes it, and
125+ * sequencer_reset() is called with it already held -- so writers are
126+ * serialized. The tick WALK, though, runs without the lock, which is safe
127+ * because the links are INDICES INTO A FIXED ARRAY, not pointers:
128+ *
129+ * - publishing a splice is one aligned 32-bit store, so a walker sees
130+ * either the old link or the new one, never half of one;
131+ * - every stored link is greater than the slot holding it, so walking
132+ * strictly increases the index. A stale link can make a walker skip a
133+ * sequence or revisit one for a single tick; it cannot form a cycle,
134+ * cannot hang, and cannot leave the array.
135+ *
136+ * So the worst a race costs is one tick's events being wrong, which is the
137+ * same class of hazard the indexed sweep already had. A list of malloc'd
138+ * nodes would be a different class entirely -- a torn next pointer walks
139+ * the render thread into freed memory.
140+ */
141+ static void active_link (int32_t tag )
142+ {
143+ int32_t * prev = & first_active ;
144+ while (* prev != -1 && * prev < tag )
145+ prev = & sequences [* prev ].next_active ;
146+ if (* prev == tag )
147+ return ; /* already in */
148+ sequences [tag ].next_active = * prev ; /* point at the tail we found... */
149+ * prev = tag ; /* ...then publish, in one store */
150+ }
151+
152+ static void active_unlink (int32_t tag )
153+ {
154+ int32_t * prev = & first_active ;
155+ while (* prev != -1 && * prev != tag )
156+ prev = & sequences [* prev ].next_active ;
157+ if (* prev == tag )
158+ * prev = sequences [tag ].next_active ; /* one store, again */
159+ }
160+
90161void sequencer_recompute () {
91162 // 60000000 us/min / (bpm * ticks per beat); keep it single-precision -
92163 // unsuffixed double literals pull in software double emulation on 32-bit.
@@ -129,6 +200,7 @@ uint8_t sequencer_add_wire(uint32_t tick, uint32_t period, uint32_t tag, bool ha
129200 sequences [tag ].wire = NULL ;
130201 sequences [tag ].tick = 0 ;
131202 sequences [tag ].period = 0 ;
203+ active_unlink (tag ); // out of the list while it has nothing in it
132204 if ((tick == 0 && period == 0 ) || // Non-schedulable event: just clear the tag.
133205 (tick != 0 && period == 0 && tick <= amy_global .sequencer_tick_count )) { // don't schedule things in the past.
134206 amy_release_lock ();
@@ -138,7 +210,7 @@ uint8_t sequencer_add_wire(uint32_t tick, uint32_t period, uint32_t tag, bool ha
138210 sequences [tag ].tick = tick ;
139211 sequences [tag ].period = period ;
140212 sequences [tag ].wire = wire ;
141- if (( int32_t ) tag > highest_tag ) highest_tag = tag ; // To limit scanning through tags.
213+ active_link ( tag ); // ...and back in, now that it has a message again
142214 amy_release_lock ();
143215 return 1 ;
144216}
@@ -150,8 +222,12 @@ static void sequencer_process_tick(void) {
150222 // while still processing this tick's fires; restore on the way out.
151223 bool was_firing = wire_firing ;
152224 wire_firing = true;
153- // Scan through the tag table looking for matches
154- for (int32_t tag = 0 ; tag <= highest_tag ; ++ tag ) {
225+ // Walk only the slots that have something scheduled. This used to sweep
226+ // 0..highest_tag, a mark that never came down.
227+ int32_t tag = first_active ;
228+ while (tag != -1 ) {
229+ // Read the link BEFORE anything below can unlink this entry.
230+ int32_t next = sequences [tag ].next_active ;
155231 if (sequences [tag ].wire != NULL ) {
156232 bool hit = false;
157233 bool delete = false;
@@ -174,6 +250,7 @@ static void sequencer_process_tick(void) {
174250 sequences [tag ].wire = NULL ;
175251 sequences [tag ].tick = 0 ;
176252 sequences [tag ].period = 0 ;
253+ active_unlink (tag );
177254 } else {
178255 size_t len = strlen (sequences [tag ].wire );
179256 wire = (char * )malloc_caps (len + 1 , amy_global .config .ram_caps_events );
@@ -189,6 +266,7 @@ static void sequencer_process_tick(void) {
189266 }
190267 }
191268 }
269+ tag = next ;
192270 }
193271 wire_firing = was_firing ;
194272 if (amy_global .config .amy_external_sequencer_hook != NULL ) {
0 commit comments