Back to a normal week after the midterm. It split cleanly in two: the first half closing out the debugger work I’d been carrying for a while, and the second half falling down a rabbit hole about film loops that I’m still in.
Closing out the debugger
The two fixes I said were nearly done last week both landed, along with a third that had been sitting on my “still on the list” note: the channel visibility toggle that did nothing while the movie was paused.
That one turned out to be a nice little bug. The debugger keeps a _windowToRedraw request, and it was being serviced inside onImGuiRender(), which runs after the frame has already been composited. During normal playback you never notice, because the next frame comes along a few milliseconds later and picks up the change. But when the movie is paused, there is no next frame. The request sat there and was serviced into a frame that had already been drawn, so the toggle appeared to do nothing at all. Draining the request in the main loop before draw() means it composites into the same frame, and the toggle works while paused.
I also gave the Cast window some overdue navigation help: a serial column in the list view, a toggle that overlays the member number on each tile in grid view, and a member count in the toolbar that shows shown/total when a filter is active. Small things, but this window is where you spend most of your time when a game’s cast is a thousand members long.
A different film loop bug
I wrote about film loops two weeks ago, but that was about where they get drawn, the registration point problem. This week’s bug is about when they move, and it turned out to be a much more interesting question.
The code that drives them is a single function, Score::incrementFilmLoops(). Every render tick it walks the channels, and for each one holding a film loop it bumps that channel’s frame counter by one. Simple, and mostly right, a film loop has no tempo of its own, so it animates in lockstep with the score that hosts it.
The problem is the “every render tick” part. It advanced regardless of what the playhead was actually doing. So a film loop kept animating while the movie was paused, and it kept animating while the playhead sat looping on a single frame. I had a suspicion both were wrong, but a suspicion isn’t a bug report.
Reading the manual
The books are the fastest way to settle these questions, except the ones I have are scans of 1990s manuals, and the OCR is rough. Searching a 670-page PDF for “film loop” returns nothing, not because the phrase isn’t there but because the OCR has rendered it as film loop, filmloop, film ]oop, and about six other things, with stray punctuation and line breaks landing in the middle of words.
sev pointed out the obvious thing I’d been ignoring; these books have indexes, and the index is a better entry point than search a lot of times.
That got me to the answer: film loops animate in step with the movie’s playback head. If the playback head isn’t moving, neither is the film loop. Director in a Nutshell (Epstein, 1999).
Asking the original
Books are good, but the actual application is better, so I built small test movies in real Director 4 and Director 5 to check each case directly. This is where it stopped being a one-line fix.
Pausing behaved as the book describes, the film loop freezes. ScummVM animated straight through it. Clear bug, and it applies to every version.
go the frame, a script that loops the playhead on a single frame, split by version. Director 4 freezes the film loop. Director 5 animates it. So this one needs a version check, not a blanket fix, and if I’d only tested in D5 I would have concluded there was no bug at all.
The tempo channel, a frame that waits a set number of seconds, was the interesting one, because ScummVM already gets it right, by accident of structure rather than by design. When the score is waiting on a tempo, isWaitingForNextFrame() returns early and renderFrame() never runs, so incrementFilmLoops() never gets called and the film loop freezes on its own. So no “fixing” here.
The fix, and checking it didn’t break anything
The change was pretty small, an early return when playback is paused, and a version gated check that tracks the last frame number the film loops were advanced on, so a playhead looping in place doesn’t count as movement in D4.
The part I want to note is the verification, because “small change” and “safe change” are not the same claim. The director-tests repo has a regression suite that runs a few hundred Lingo assertions across a stack of test movies. I ran it against my build: 196 pass, 11 fail. That number alone tells you nothing, you need to know what it was before.
So I checked out the parent commit, rebuilt clean master, and ran the identical suite: 196 pass, 11 fail, and the failing assertions were byte-identical once sorted. All eleven are pre-existing failures in event and play tests, none of which involve film loops.
That’s the difference between “I think this is fine” and “this changes nothing else,” and it’s the version I can put in a commit message.
Archaeology: this was tried once already
The reason I care about the film loop code is that it’s the foundation for the thing I’m working on now, movie cast members, which are Director’s way of embedding a whole linked movie as a single sprite. Where a film loop is inert animation, a movie cast member keeps its own scripts and sound. ScummVM’s MovieCastMember currently inherits from FilmLoopCastMember and overrides almost nothing, so today it renders as a silent, non-interactive animation and the flag that says “run this movie’s scripts” is parsed and then ignored.
Before starting I went looking for prior art and found sev had a director-moviecast branch. It took a while to work out what happened to it, because the merge base pointed somewhere confusing, the answer is that it was merged and then reverted. The revert message is the useful part: the approach drove the embedded movie by calling step() on its score, which was gated by _nextFrameTime and had unsafe side effects, including resetting video playback. It fixed a walking animation in Mission to Planet X and broke other things.
sev’s view is that movie casts need rewriting from scratch on the film loop approach, and having read the revert I understand why. Knowing exactly where the previous attempt hit the rocks is worth more than a clean slate.
Still on the list
Starting the rewrite itself. The design question I’ve been chewing on is how an embedded movie “runs at the same time” as the movie hosting it, and the answer is that it doesn’t, not in the way the phrase suggests. Director’s concurrency is cooperative, the main loop steps the stage’s score one tick, steps each open window’s score one tick, and composites once. Nothing runs in parallel; everything is interleaved on a single thread, and the deterministic ordering that gives you is a feature, because scripts fire at defined frame boundaries.
Tempo works the same way, as a clock rather than a counter: each score records the wall-clock time its next frame is due, and holds its current frame until that time arrives. Two scores at different tempos coexist because each gates on its own deadline. So an embedded movie doesn’t need a thread and doesn’t need to skip frames, it needs its own deadline, and one step per tick when that deadline comes due.
So after I discuss further with sev and have a solid plan of what to do, I’ll start the coding.
Gus Updates??
Gus Goes to The Kooky Carnival is now bug-free (from my testing). Once sev green lights it, it will be put into release. Other than that I am shifting my focus to movie cast members, away from gus games. for now.
That was the week 🙂