GSoC 2026: Director Engine and Visual Debugger

Hi, I am Ramyak. My work centered on the Macromedia Director preservation effort in ScummVM: extending the ImGui-based visual debugger (DT), fixing engine compatibility bugs surfaced by the Gus games, implementing embedded movie cast members, and building screenshot-diff tooling into the ScummVM buildbot. All links below point to the pull requests.

Please refer to the links at the end of this post for weekly breakdowns.

Key deliverables

  • Movie cast members (#7752):
    embedded Director movies that run as parallel sprites, managing separate execution states while sharing global variables.
  • Visual Debugger:
    • Major features and changes in debug tools like:
      Lingo execution tracer (#7816):
      a flamegraph view inside the debugger that records Lingo execution for performance and control-flow analysis.

  • ImageDiff on buildbot:
    brought screenshot-diff regression testing into the ScummVM buildbot dashboard, plus a set of Director regression test movies, so rendering changes can be caught visually.
pic of the tool from dev chat
  • Games compatibility:
    Mostly for gus games: detection entries and a series of engine fixes (film loops, mouse dispatch, digital video, sound channels) that make the Gus titles playable. Some work was also done on trektech. The goal initially also included The Journeyman Project but since there were a lot of bugs in gus games, I did more work there instead.
movie paused at a breakpoint

Before the coding period I made around 10 contributions building the initial Director visual debugger and fixing film loop rendering, which laid the groundwork for the GSoC work.

Most of the PRs below are merged, three are in review.

Director visual debugger (DT)

  • #7553 Fix bugs and crashes throughout the visual debugger
  • #7564 Improve search and add a Windows panel
  • #7577 Improve cast details, add script browser navigation and keyboard shortcuts
  • #7612 Fix script not rendering in the script viewer
  • #7646 Fix crashes, stale state, and navigation in the debugger
  • #7690 Call-stack highlighting, Cast window improvements, and paused redraw
  • #7781 ImGui debugger improvements (sortable cast lists, thumbnail refreshing)
  • #7816 Add a Lingo execution tracer that records execution as a flamegraph (in review)

Director engine fixes and features

  • #7563 Fix empty cast slot error and immediate sprite drag in D4+
  • #7598 Add detection entries for Gus Goes to Cyberopolis (Windows and Mac)
  • #7614 Add detection entry for Gus Goes to Cybertown
  • #7620 Fix film loop shift when swapping a cast member with Lingo
  • #7633 Fix mouseUp dispatching to the wrong sprite in D4
  • #7727 Fix film loop D4 and D5+ behavior (freeze during pauses)
  • #7752 Implement movie cast members as parallel interactive movies
  • #7780 Fix trail-sprite erasure and 0x0 digital-video sizing
  • #7825 Share sound channels across the Stage and MIAWs (in review)
  • #7854 Fix Lingo point/rect operations and bitmap picture assignment (in review)

Backend

  • #7779 BACKENDS: SDL: Preserve SDL text input for active ImGui text fields

ImageDiff and buildbot infrastructure (scummvm-sites)

  • #39 Integrate the ImageDiff screenshot diff viewer
  • #40 Add pagination and default to the last page
  • #41 Replace image comparison with a file check
  • #42 Add ImageDiff integration using the wsgi-dashboards plugin
  • #43 Fix imports after merging imagediff.py into main.py

Director regression test movies (director-tests)

  • #4 D4-win: Text wrapping test movie with reference rendering
  • #5 D4 test movies for mouseUp dispatch and text wrapping
  • #7 D4-win/D5-win: clickOn manual test movie in both D4 and D5

Timeline

  • May 2026: Community bonding, Introduction
  • Jun to Jul 2026 (Weeks 1 2 3 4 5 6)
    Debugger fixes, Gus game compatibility, ImageDiff on buildbot; midterm passed mid-July.
  • Jul to Aug 2026 (Weeks 7 8 9 10 11 12)
    Movie cast members, film loop behavior, Lingo execution tracer, final polish

GSoC 2026 Final Report

Porting iBASS engine to ScummVM 

Organisation: ScummVM

Contributor name: Priyanshu Kumar

Coding period: 25 May 2026 – 17 August 2026

Over the past few months, I had been working on implementing support for the remastered version of Beneath a Steel Sky in ScummVM’s sky engine and in this final report, I am giving a brief description of what has been done, what is underway and what is yet to be done.

The goal of this project was to incorporate the changes done by the developers of iBASS into the sky engine upstream. That included the enhanced ui icons, separate inventory pathway, enhanced cutscenes including the intro and outro and many more things which I am happy to say that I have largely completed. Now, the game can be played from beginning to the end with minor bugs remaining that I will be working on fixing in the near future.

What’s completed(attaching the work below)-

1. Initial support – Added ibass to detection, overlayed the icons, implemented the mouse input pipeline for ibass and some work on the inventory.

PR – https://github.com/scummvm/scummvm/pull/7565

2. Implemented text chooser

PR – https://github.com/scummvm/scummvm/pull/7634

3. Skip checking for the sky.dnr file for ibass

PR – https://github.com/scummvm/scummvm/pull/7640

4. Fix transparency in inventory icon

PR – https://github.com/scummvm/scummvm/pull/7641

5. Added CgBI decoder to decode the CgBI PNGs for the hint system’s window and implemented the hint system.

PR – https://github.com/scummvm/scummvm/pull/7811

6. Implemented the intro and outro cutscenes and also stretched the graphics to 480X320.

PR – https://github.com/scummvm/scummvm/pull/7850

7. Added hotspots

PR – https://github.com/scummvm/scummvm/pull/7853

And here is the link to all the blogs I wrote during this journey –

https://blogs.scummvm.org/priyanshu/

PRs 5, 6 and 7 are unmerged at the time of writing this report and are subject to review.

What’s yet to be done –

1. There are a few crashes here and there that I need to identify and fix.

2. The hint system needs some more refinement.

3. Support for different languages in the hint system.

4. Extensive testing after stretching the graphics to ensure smooth transition of the game to 480X320 and fix the discrepancies on the go.

This sums up my GSoC 2026 journey and I learnt a lot in these past few months. I would like to extend my gratitude to the whole ScummVM team for supporting, guiding, helping and keeping a check on my work. Special thanks to Eugene Sandulenko and John Willis for mentoring me and for always being there when I needed their help. Looking forward to continue working with such a wonderful and passionate group of people on more such projects.

And to all my readers and those who want to defeat LINC-

BE VIGILANT 😉

Thanks for reading 🙂

Avalanche: End of the Slope

Hi everyone, welcome back! This is the blog for week 12 — and the last one. My GSoC journey comes to an end with this post. This week I implemented the missing features of the Avalanche engine and tackled some bug fixes — and with that, the Avalanche engine is complete! This is a quick post to meet the Tuesday deadline — a detailed blog covering both the DOS version of Dungeon Master and the Avalanche engine is coming tomorrow.

Till then, goodbye and thank you for reading my blogs! 👋

Final Week

This week was mostly about one thing: finishing the remaining Gus game bugs.

Gus Paint

One of the bugs was in the inside() Lingo function, which was causing the Paint and Play buttons in Gus Paint to crash. I found the fix, but it was slightly wrong in its assumptions. I figured that out a couple of days later when I looked at it again and made the required change.

The game passes a proper POINT and the rect of cBackground, but cBackground is temporarily VOID during the first few frames. This meant inside() received (POINT, VOID) and fell through without returning a value, causing a fatal error.

Fixed it to return FALSE for invalid arguments instead of crashing. Sev also suggested using the existing TYPECHECK macros for handling the different argument types.

There were some other smaller bugs I worked on, like a textbox getting disproportionally larger incrementally with text input. funniest bug so far. fixed it. This one along with some more bugs, will be listed in the PR.

I spent a lot of time simply going through the games and reproducing things that looked wrong.

Over the last few weeks I have gradually gotten better at narrowing those problems down. Instead of immediately assuming that the game itself is doing something unusual, I can now usually trace the failing Lingo call into the engine and figure out what assumption ScummVM is making that the original Director did not.

Looking back

This week was mostly about cleanup: playing through games, finding the remaining failures, tracing them into the engine, and getting them into a state where they can be fixed properly.

And with that, GSoC is coming to an end.

Over the last 12 weeks I have worked on a fairly wide range of things in the Director engine: the ImGui visual debugger, movie cast members, film loops, Lingo debugging and profiling, ImageDiff and buildbot integration (-_-), game detection, and a lot of small engine and game compatibility fixes.

It has been a pretty different experience from working on a normal project. A lot of the time I was dealing with code and file formats that are decades old, trying to figure out not just what the original game is doing, but what Director itself must have been doing underneath it.

I’ll soon be posting a separate write-up covering the work I did over the full 12 weeks.

Because they need some testing, I”ll push the PR for the bugs soon. (Updating soon)

Upadate: https://github.com/scummvm/scummvm/pull/7854

that’s it for week 12 : )

Week 13

Welcome back to the blog. The intro and the outro videos have been implemented and the PR is created. I remember writing in the last week’s blog that I will start testing the videos on the qtdecoder but the video and audio codecs of that format were not supported on scummvm’s qtdecoder. So, the mentors suggested re-encoding the videos to one of the formats that have their decoders implemented in scummvm and sev gave me some options out of which I chose to re-encode them to svq1(one of the reasons being that I was too lazy to write the decoding pipeline for another format and since I already had written it for qtdecoder to test the original assets and that svq1 was one such format that qtdecoder decoded😅). And then we got the intro-

Can’t post the whole intro since it will be too long but here is the beginning.

And this is the outro-

“Copyright 1994 Revolution Software Limited” marks the end of the post credits and then the outro starts.

These intro and outro videos have a resolution of 480X320. sev suggested to keep that resolution and stretch the graphics of the game to match that instead so that it makes the inventory icons look better on the screen as opposed to previously, wherein they appeared a bit large.

This is how they look now-
And the final evaluation timeline has officially started. So, I will now be writing the final report detailing about what all has been done, linking to the PRs and what is yet to be done.

Thanks for reading 🙂

The final post

GSoC 2026 Final Report — Finishing Incomplete ScummVM Engines

Contributor: Ion Andrei Cristian · Organization: ScummVM · Coding period: 25 May – 17 August 2026

This is my final work product for Google Summer of Code 2026. It collects everything I worked on this summer in one place: what the project set out to do, what actually landed, where things stand now, and what is still open for whoever picks this up next.

THE PROJECT

ScummVM has a number of engines that are almost finished — the games boot, most of the logic is there, but something keeps them from being shipped: incomplete low-level graphics, legacy code structures inherited from a decompilation, or gameplay bugs nobody has sat down and tracked to the end. My proposal listed several such candidates. My mentor’s guidance was clear and, in hindsight, exactly right: rather than touching many engines superficially, take two of them all the way to a releasable state.

The two were:

  • Chamber of the Sci-Mutant Priestess — a 1989 adventure by Delphine Software, whose ScummVM engine had a working CGA path but a broken EGA one, no Amiga support, and a long tail of gameplay bugs.

  • MacVenture — the engine behind Déjà Vu, Déjà Vu II, Shadowgate and Uninvited, which ran but crashed regularly and diverged from the original in ways that made the games unpleasant or impossible to finish.

One thing changed along the way. Chamber’s Amiga releases turned out to be a much larger and more interesting piece of work than anticipated — a whole second renderer, with planar graphics, a different palette system and two separate regional releases. I discussed it with my mentor and we agreed it was worth doing properly rather than skipping. It became roughly a third of the summer.

WHAT I DID

Chamber of the Sci-Mutant Priestess

Getting EGA right. The EGA renderer was the headline problem. I fixed detection MD5s and title screen rendering, a transition that took twenty seconds on player death, mouse click coordinate latching, cursor hotspots, and implemented a missing script opcode. Later came a series of memory-layout bugs where EGA’s different buffer geometry caused writes to overflow into neighbouring structures and corrupt sprite lists and the backbuffer.

Gameplay and scripting. An infinite “you failed the ordeals” death loop caused by a timer bleeding across rooms. An endgame confrontation menu that re-prompted forever because a priority command never restored its stack pointer. An opcode whose operand width was wrong by one byte, silently desynchronising the whole script stream after it. Stale actors and commands surviving room transitions.

Randomness. The engine’s randomize() was a stub, so the seed was always zero and the sequence identical on every run — which made the same character spawn first in every single playthrough. The original seeded from the BIOS timer tick, and that seed is only the starting offset into a fixed table the game walks for its random values. I kept the table, so the distribution still matches DOS exactly, and took the offset from Common::RandomSource instead — which, on my mentor’s suggestion, also means the random_seed config key makes a run reproducible when you are chasing a bug.

The Amiga port. This was the biggest single piece. I reverse-engineered the Amiga data formats — 16-colour four-plane bitplanes, 12-bit RGB palettes, the SPRIT/PUZZL sprite containers with their byte-swapped dimensions, the cursor format — and built a renderer that shares the EGA chunky pipeline while differing in palette and planar conversion. Then the same again for the US “Chamber” release, whose executable has a completely different static-resource offset table. Both the European “Kult” and US releases are now playable start to finish.

Save/load. Scripted room changes — the De Profundis monster, Deilos — lived only in the backbuffer and vanished when you reloaded. Version 2 of the save format stores the backbuffer.

Release work. Static analysis passes (Coverity and PVS-Studio findings), a detection fix for folders containing both CGA and EGA data (bug 17004), engine enabled by default, all variants promoted to testing.

MacVenture

Getting the data in. The Steam re-releases wrap the game data in a CEF application, with an HFS disk image buried in a PE resource. I wrote a devtools extractor for it, extended it to the Apple IIGS disks, and fixed IIGS detection, which had been silently dead because there was no data-fork fallback.

Crashes. Inventory windows closed out of order, a use-after-free where a window callback deleted its own data while the engine kept dispatching through the dangling pointer, an unsigned coordinate wrapping when you dragged an object off-screen, a double free in the CALL opcode when a script called a function that does not exist, a double delete in the text-input dialog. Plus one in shared MacGUI code, where past roughly 680 lines of scrollback the rectangle maths overflowed a 16-bit Common::Rect.

Making it behave like the original. Inventory window placement and sizing, lasso selection picking the wrong objects, Clean Up throwing items outside their window, the watch cursor while a command is processed, “click to continue” console paging, dialog buttons that invert while held and only act on release, shift-clicking to select several objects at once, and the diploma at the end of Déjà Vu actually being signed with the player’s name.

Timing. The SLEEP opcode computed (ticks / 60) * 1000, truncating every sub-second pause to zero, so animations flew past. And the GUI was forcing a full-screen refresh every frame, running the game at 17 fps instead of 50.

Two engine-level corrections. The random opcode returned a value inclusive of its maximum where the original returns one strictly below — every script indexing a table with it could read one past the end. And commands could not target the object they were invoked on, because a workaround for a duplicate-execution bug skipped the destination entirely; the real fix was to keep the destination out of the selection queue in the first place, as the original does.

Shared ScummVM code

Not everything was engine-local. Two fixes landed in shared code: an invalid-rectangle bug in the Mac GUI text renderer, a new absolute scrollTo() on MacTextWindow, and a SurfaceSDL regression that drew the game cursor several pixels off-target at scale factors above 1x — which affected every engine, not just mine.

CURRENT STATE

Chamber of the Sci-Mutant Priestess is enabled by default and all five variants are marked as testing. It is completable on CGA, EGA, Hercules, and on both the European and US Amiga releases. The Steam release is detected and playable.

MacVenture is in review for the same treatment. Déjà Vu is completable from start to finish. Déjà Vu II has been played to within sight of the ending and every blocking bug I hit along the way is fixed; the last stretch of the playthrough is the one piece of testing I did not get to finish. The two pull requests that enable the engine by default and promote the Macintosh releases to testing are open at the time of writing.

WEEKLY BREAKDOWN

Every week of the coding period has a blog post describing the work in detail. The pull requests for each week are listed alongside.

Week 1 – Focus: EGA detection, rendering and input; a rigged minigame and a frozen snake

Post: Week 1 Pull request: 7530

Week 2 – Focus: RNG seeding, ordeal timer death loop, state leaking across rooms, opcode operand width

Post: Week 2 Pull request: 7566

Week 3 – Focus: Confrontation menu stack overflow, sprite assembly coordinates, EGA zone transitions

Post: Week 3 Pull request: 7586

Week 4 – Focus: EGA memory layout overflows, timer endianness, zone scan width

Post: Week 4 Pull request: 7597

Week 5 – Focus: Amiga EU renderer: palette, sprite banks, script padding — playable end to end

Post: Week 5 Pull request: 7607

Week 6 – Focus: Amiga US release support, detection entries, zone scan effect; SDL cursor hotspot fix

Post: Week 6 Pull requests: 7629, 7628

Week 7 – Focus: Coverity and PVS-Studio findings; save format v2 storing the backbuffer

Post: Week 7 Pull request: 7629

Week 8 – Focus: Chamber enabled for release and promoted to testing; first look at MacVenture

Post: Week 8 Pull request: 7705

Week 9 – Focus: Steam and IIGS extraction tooling, IIGS detection, an uninitialised field causing assertions

Post: Week 9 Pull requests: 7722, 7726, 7728, 7748

Week 10 – Focus: Playing Déjà Vu through: Clean Up coordinates, console scrolling, watch cursor, animation pacing

Post: Week 10 Pull request: 7760

Week 11 – Focus: Inventory crashes, lasso selection, 17→50 fps, the signed diploma, click to continue

Post: Week 11 Pull requests: 7773, 7784, 7787, 7807

Week 12 – Focus: Déjà Vu II: script crashes, operate-on-itself, shift click, dialog buttons; release packaging

Post: Week 12 Pull requests: 7820, 7830, 7834

All pull requests are merged except 7830 and 7834, which are open and awaiting review.

WHAT IS LEFT TO DO

I would rather be honest about this than leave someone guessing.

In review. 7830 (enable MacVenture by default, add credits) and 7834 (promote the Macintosh releases to testing) are open. They are the last step before MacVenture can ship.

Finish the Déjà Vu II playthrough. I got very close to the ending, and the run was clean by that point, but the final stretch has not been played. It is a short job and the obvious first thing for anyone continuing this work.

Click to continue needs polish. The console now pauses and pages through long messages the way the original does, and it works — but the interaction is not yet as smooth as the real thing in every situation. It is functional, not finished.

Five sounds in Déjà Vu II do not play. Fifteen of the twenty work. The remaining five are not audio data at all: they are 68k CODE resources that the original played by executing them. Supporting them needs actual 68k emulation, which is well outside the scope of a bug fix.

Drawing mid-script is unsafe. MacVenture only assembles a fully consistent screen at the end of its main loop. Any attempt to draw from inside a running script (the opdeUPSC path) trips over half-built windows and invalid rectangles. I worked around it by pacing the main loop on elapsed time, which fixed the animations I needed, but the proper fix is a real animation path through the engine’s update cycle. Anyone attempting it should expect the crashes I did.

WindowData::bounds mixes coordinate systems. It holds content coordinates in some places and screen coordinates in others. I left this deliberately: updateWindow’s fillRect and the “mess up” logic both depend on the current behaviour, and untangling it safely is a refactor of its own rather than something to slip into a bug-fix branch. It is the single biggest source of coordinate confusion in the engine and worth doing properly.

Shadowgate and Uninvited need a full playthrough. MacVenture supports four games. I gave both of these a short look — they start up and play fine as far as I took them — but neither has had the sit-down-and-finish-it treatment that Déjà Vu and Déjà Vu II got. Given how much that turned up in the other two, I would expect a similar list from each.

Chamber has no per-language text support. The engine only distinguishes English (EN_USA) from non-English, so the French and German releases cannot be presented properly. Adding real per-language handling needs engine work, not just detection entries.

CHALLENGES AND LESSONS LEARNED

Coordinate systems were the theme of the summer. Clean Up throwing items out of their windows, lasso selection grabbing the wrong objects, inventory windows cascading off the bottom of the screen, the SDL cursor drawn several pixels off — all the same class of bug. What finally made the lasso one fall into place was not a clever insight, it was writing down explicitly which system each value was in: the mouse event is relative to the outer window, objects are relative to the content area plus scroll position. Once that was on paper the fix was obvious. I now do this first rather than last.

Reverse engineering without ground truth is slow, and you should go get ground truth. I spent a long time on the Amiga sprite bank encoding, trying chunky and every planar layout I could think of, and getting noise every time. The productive move — which I got to later than I should have — was to stop guessing at the format and go look at what the original executable actually does. Guessing scales badly; disassembly does not.

Old workarounds hide real bugs. My favourite fix of the summer was the Déjà Vu II flashlight doing nothing when you operated it. The cause was a workaround someone had added for a duplicate-execution bug: skip the destination while running the selection queue. It stopped the duplicate and also broke every command whose target is its own source. The lesson is that when a workaround produces a second symptom, the workaround is usually the thing to remove, not to extend.

Playing the game is the best bug report you can write. Weeks 10 through 12 were the most productive of the summer, and all I did was sit down and actually play the games with a notebook. Every bug I fixed in that stretch came from noticing something felt wrong, not from reading code — the same way I had found most of Chamber’s.

Check your build flags before you blame the engine. I lost time chasing jerky, stuttering gameplay that turned out to be a build configured with –enable-debug and no optimisations at all. The engine was fine.

Static analysis is worth a dedicated pass. Handing Coverity and PVS-Studio output a full afternoon surfaced real dead stores, leaks and uninitialised reads that no amount of playing would have found.

PRIOR CONTRIBUTIONS

For completeness, and to be clear about what belongs to the GSoC period and what does not: I was contributing to Chamber before the coding period began on 25 May. These pull requests are not part of my GSoC work, but they are the foundation the summer built on.

7267 · 7270 — splash screen refactor and Hercules palettes moved to the global graphics manager

7294 — Hercules scaling

7440 — initial EGA rendering

7474 — save/load support

7479 — splash screen filenames moved to detection flags

ACKNOWLEDGEMENTS

Thank you to my mentors, especially sev, for twelve weeks of steady guidance, quick reviews and genuine patience — including the times I turned up with a coordinate bug I had already been warned about. Thank you also to the wider ScummVM community for the review comments, and to Google for running the program.

Twelve weeks ago I had never touched either of these engines. Both are now heading for a release. That still feels slightly unreal.

Week 12

Last week I fixed the remaining problems with Déjà Vu, and  this week I opened Déjà Vu II and started again from the top, notebook in hand. It is the same engine, the same tooling, and the same approach as before, but a different game exercises different corners of the code — and it found plenty of them. This being the final week, it was also time to stop fixing and start packaging.

Two crashes hiding in the same week

The first one turned up almost immediately. Using Hit on an object that has no hit handler crashed the game outright. The CALL opcode was popping the script list unconditionally once the callee had run, but the loader only pushes a script when the function actually exists. Call something that isn’t there, and the engine cheerfully popped the caller instead, leaving the reference the interpreter was still holding dangling — and the next assignment freed the same instruction array a second time. The original engine reserves the slot before the call and always removes that same slot, so the list stays balanced either way. Now the pop only happens when a script was really pushed.

The second was much shorter to write down: the routine that asks the player for text deleted the open dialog without clearing the pointer, then called into code that begins by closing the dialog and deleting that very same pointer. Two lines removed, one double free gone.

The flashlight that did nothing

This was my favourite bug of the week, because the fix was really an apology for an older workaround.

In Déjà Vu II you find a flashlight, you click Operate, you click the flashlight — and nothing happens. The cause went back a long way: the destination object was being pushed into the selection queue, so every two-object command also ran a second time on its own destination. Someone had patched around that by skipping the destination while running the queue, which does stop the duplicate — and also makes any command whose target is the source do precisely nothing.

The real fix was to stop putting the destination in the queue in the first place and track it in the highlight list instead, which is what the original engine does. Operating an object on itself works again, and the duplicated command stays gone.

Selecting more than one thing

Shift-clicking is supposed to add an object to the selection rather than replace it, so you can grab a handful of items and drag them together. Dragging already knew how to handle groups; only the selecting was missing, and it was missing in three places at once. The shift state never reached the engine because the cursor code always passed false. The selection call passed its last two arguments in the wrong order, so the shift flag arrived where the double-click flag was expected. And the shift branch itself was still an empty stub.

Fixing the argument order was the interesting part, because it immediately exposed a second call that had been quietly landing in that empty branch on the release of every single click. With the flags the right way round it suddenly started activating objects instead. It turned out to have no other purpose, so it is now gone.

Small corrections, real consequences

Two one-liners worth mentioning. The random opcode was returning a value between zero and the maximum inclusive, where the original returns a value strictly below it — which means every script indexing a table with that result had a chance of reading one entry past the end. And object updates were being dropped whenever the object already had an entry in the queue, except the queue also holds window entries, which are dispatched later. While one of those was pending, an object could change without its window ever being told, so the change only appeared once you re-entered the room.

Buttons that feel like buttons

Dialog buttons were firing their action the instant the mouse went down, and never showed that they were being held. The original inverts a button while it is pressed, de-inverts it when the pointer leaves, and only acts on release inside the bounds — so a misplaced click can still be taken back by dragging away before letting go. The action is now also tied to a press that started on that same button, so a stray release left over from whatever opened the dialog can’t trigger one.

The one that got away

Some sounds in Déjà Vu II still log “unrecognized sound type”. I spent a while on it before working out that those entries are not audio at all: they are 68k CODE resources, and the original played them by executing the code. Fifteen of the twenty sounds play correctly; the rest would need actual 68k emulation, which is well beyond a bug fix. It goes on the list as a known limitation rather than a regression.

Shipping it

With the playtesting done, the last commits of the summer were the boring, satisfying ones: the MacVenture engine is now enabled by default in configure, the Macintosh releases are promoted from unstable to testing, and my name went into the engine credits.

That’s a wrap

And that is the twelfth and final week. I will be putting together a proper final report shortly — one page with everything I worked on this summer, every pull request, and an honest list of what is still left to do — and linking it from here.

Link to the final post

Twelve weeks ago I had never touched either of these engines. Chamber of the Sci-Mutant Priestess now runs in EGA, CGA, Hercules and on Amiga, and MacVenture is heading for a release. I have learned more about coordinate systems, byte order and other people’s workarounds than I expected to.

Huge thanks to my mentors for the steady guidance and patience all summer — this was genuinely a great one.

Lingo Tracer

So the highlight of this week is, as the title suggests, the Lingo tracer. I mentioned it in a recent blog. When I shared a screenshot with sev of the prototype I made, he said it seemed like a good idea and green lit it. This week was spent mostly on it, and getting it working was not that hard. The hard part was making it easy to use and understand.

The tracer records Lingo as it runs: every handler that gets called, when the score changes frames, and when a script is frozen and later resumed. It draws all of that as a flamegraph inside the ImGui debugger, so you can see which handlers ran, how they nested, and how long each one took. There is also a small stats table that sums the total and self time per handler, so the expensive ones are easy to spot. It stays off until you tick Capture, so it costs nothing when you are not using it.

Most of my time went into the reading experience. Frames and freeze/thaw markers sit in a thin strip at the top so the flamegraph below stays clean, tiny sub-pixel slices get merged so a busy trace does not turn into noise, and hovering one call highlights every other call with the same name.

fix

The other fix this week was a sound bug in gusmuse. Entering or leaving an area stacked the music tracks on top of each other, and the previous one never stopped.

In Director the sound channels are one shared resource between the Stage and any movie-in-a-window, but in ScummVM each window had its own sound manager, so a track started from one window could not stop a track from another.

I routed every window’s sound through the Stage’s manager, and now the channels are truly global.

That’s it for the week : )

Week 12

Starting to write this blog with a great news that the PR implementing the CgBI decoder as well as the hint system has been created. I just finished the last touch up of the hint system and then pushed it. How long it takes before it gets merged now is subject to review.

While implementing the hints, I encountered many cases where, due to many hints for a single question, the hints did not fit on the screen all at once. So, I implemented a button to go down to the bottom so that the user can view the hints fully.

This is how the hint system is working. As you can see, the moment the hints go beyond the bottom edge, the “down” button appears.

Next, I will be implementing the intro and outro videos. Those videos are in a format named “m4v”, which, as I understood by reading about it, is just an mp4 container only but with a different extension, created by apple. It uses the aac audio and h264 video codec. I am not completely sure if our existing decoders would be able to decode this format but during the time I had started working on this project, I’ve had a discussion with @sev on this topic and he thinks that our quicktime decoder would be able to decode it.

So, I will start attempting to decode it with quicktime only.

Thanks for reading 🙂

Week 11

Last week ended with Déjà Vu playable from beginning to end, and two things left on the side: the elevator doors and the console pagination. This week was spent turning the rest of my playtesting notes into patches, most of them already merged, and finally crossing off both of those lingering issues.

Squashing the crashes

Three of the crashes I kept running into all came from the same corner of the engine: the inventory windows.

One was a simple issue of the engine assuming windows are closed in the exact order they were opened. Close one in the middle, and it would ask for a reference that no longer existed. Another was a nasty use-after-free: the window callback deleted its own data when closed, but the engine kept dispatching events through a dangling pointer until the next cleanup sweep. Whether the game crashed or not depended entirely on what reused that memory, making it look completely random. The third happened when dragging an object past the edge of the screen, which wrapped an unsigned coordinate and caused the engine to try and allocate a massively oversized surface.

I also tracked down a crash in the shared Mac GUI code. The MacVenture output console keeps the whole session’s scrollback, and once you passed about 680 lines of text, the rectangle calculations overflowed a 16-bit Common::Rect and tripped an assertion. Fixing this required teaching the shared drawing code to handle destination coordinates and clamping properly.

Taming the inventory windows

New inventory windows were piling up on top of each other and growing out of control. The placement code was inheriting both the size and offset of the previous window, so by the time you opened a third window, it was already rendering below the bottom of the screen. Now, the size comes strictly from the settings, and the offset is based on the number of open windows.

Lasso selection inside these windows was also picking up the wrong objects. This is the exact coordinate problem I ran away from last week! The mouse position was relative to the outer window, but the objects were placed relative to the content area, with hardcoded vertical corrections sprinkled in. Having the two coordinate systems written down explicitly was what finally made it fall into place.

Pacing the game correctly

I ran into two completely opposite timing problems this week.

First, the overall game was chugging along at about 17 fps instead of the intended 50. The GUI was forcing a full screen refresh and redrawing the contents of every window on every single frame, eating up 60 ms. Now, window contents are only redrawn when an event, a command, or a script has actually changed them.

On the other end: remember the elevator doors that animated too fast? I finally fixed them. Instead of trying to draw mid-script (which proved unsafe last week), the main loop now paces frames by actual elapsed time rather than a fixed 50 ms delay. The elevator doors now open exactly as they should.

(Bonus: I also fixed a bug where dragging an item ran the command a second time with the destination as the source, printing a nonsensical “X does not have any effect on X”.)

Signing the diploma

At the end of Déjà Vu, you are handed a diploma and asked to type in your name. The original game puts your name right on the diploma itself, but nothing in the engine handled this. It turns out the resource describing the name line (kDiplomaGeometryID) was already defined in the sources but never actually read. Reading it was enough to sign the diploma, and the Print button now correctly hands the signed document to the printing manager.

(This also came with a one-line parser fix nearby, where zero-length strings could leave uninitialized pointers on the stack).

Bringing back “Click to continue”

This was the second issue that got away from me last week. The original game stops printing when the output window is full and waits for a click. ScummVM had the code for the prompt, but it almost never appeared, and when it did, the game froze permanently.

Fixing this took three separate changes: preventing internal state resets from clearing the pending pause counter, ensuring the main loop continues running to process the click, and scrolling the console one windowful at a time instead of dumping the whole message past the player. For that last part I had to add an absolute scrollTo() function to the Mac GUI, which also marks the text as dirty so the window actually redraws at the new position.

Next week

Déjà Vu is finally done as far as playtesting goes. Next up is Déjà Vu II using the exact same approach: play it through, write down everything that looks wrong, and work through the list.

It feels incredibly surreal to say this, but there is only one more week to go in the GSoC program! I’ll be spending it polishing up the rest of the MacVenture titles and getting everything ready for the final submission.

As always, thanks to my mentors for the steady guidance and patience — onward to the final stretch!

DOS and DONE

Hey everyone, welcome back! This is the blog for week 11.

Finally, the work on Dungeon Master is over and the PR is merged. ScummVM now supports both the Amiga and DOS versions of Dungeon Master, and we will very soon be inviting users for playtesting of the game.

For the Amiga version, I played through the full game, exploring every level and every corner, fixing bugs along the way. For the DOS version, I took a shorter path and tested specific areas that I suspected might be breaking, to save time. Let’s hope it’s as perfect as the Amiga version!

I have now begun work on the Avalanche engine. With exams starting Monday and the GSoC final submission also around the corner, it is going to be a hectic stretch. Fingers crossed I survive! 🙂

Meet you in the next blog! 👋

All Walls Standing

Hi and welcome back! This is week 10 of my GSoC project, and I’m happy to announce that the DOS version of Dungeon Master is almost complete. Finally!

The big focus this week was getting the dungeon walls to finally appear in the right places on screen.

The Layout Engine

Rather than hardcoding where every wall, floor, and ceiling should appear on screen, the DOS version used a data driven layout engine — essentially a small tree structure where each element knows its position relative to its parent.

How It Differs from the Amiga Version

On the original Amiga and Atari ST, wall screen coordinates were stored in a simple, hardcoded array compiled directly into the game binary.

The DOS port, however, moved all of this coordinate data out of the code and into a dedicated configuration resource inside the graphics data file (known as Graphic 696).

Instead of drawing walls using fixed table entries, the DOS engine parses Graphic 696 at startup to build a dynamic hierarchy tree, where every wall distance (D0 through D4) asks the layout tree for its relative X and Y offsets, width, and height.

With that, most of the graphics are now in place and I have begun playtesting. All monsters and explosions look perfect. Here are some screenshots:

Look at the shadow of the mummy’s hands on the back wall 😄 so much detail in this game!

I’ll upload a detailed blog covering all the code changes for the DOS version once it is complete. I know I said the same thing last week — I’m currently busy with university labs and exams, but once the final PR is up, the blog will follow!
That’s it for this week — see you in the next one! 👋

Small fixes and debugger polish

Mostly a normal week: two engine bugs, and a lot of sharpening the ImGui debugger.

Bugs.

Fixed the Cybertown cyberbud trails vanishing. The dirty channel check was comparing the raw _thickness byte, so a flipped tween bit falsely dirtied the channel and erased the trail. Fixed the Sink-or-Float “Prof. Gus’ face” (Talked about it here: week 5): a digital-video sprite the score sizes to 0×0 (an invisible timing clock) was being blown up to its native 240×180, so now it stays invisible. Also null-guarded a crash in endOfVideo().

Debugger (DT).

A sweep of bugs and quality of life fixes: killed ImGui::Text format string crashes in the script viewer, added some keyboard shortcuts, cached the cast browser rows, made the cast list sortable, added a Vars filter and copy value, refreshed stale thumbnails, added a movie cast member score viewer and a lot of other changes.

Lingo profiler.

I also spent part of the weekend on an experimental feature (for fun), a profiler for Lingo. It is still early, and I will come back to it later. For now the focus is wrapping up the Gus games.

Gus games status

  • gustown, guscarn: done
  • gusolis: 90% done
  • gusmuse: in progress
  • guspaint, guspark: remaining (still need to check how many bugs survive all the recent changes)

PRs

https://github.com/scummvm/scummvm/pull/7780
https://github.com/scummvm/scummvm/pull/7781
https://github.com/scummvm/scummvm/pull/7779

Week 11

Welcome back to another blog. Last week, I primarily worked on the text rendering inside the help panel that includes hint questions and their respective answers(hints).

The help panel provides us with hints in the form of questions and clicking on each question provides us with some hints in the forms of answers to those questions. I am currently testing the whole pipeling with one test case i.e. “How do I get out of the furnace room?” which is the case 3 in the hint system’s state machine implement in the function Control::buildHints() in control.cpp.

The question and answers are implemented in the form of a struct Hint that contains an integer for storing the question’s number and an array of integers for storing the answers indices. The questions and answers are implemented in an array of strings in ibasstext.cpp

And the font rendering is done in a method named initHelpPanel(). For the rest of the questions and answers, we have the placeholder(“?”). The hint system for the furnace case looks like this-

 

Clicking anywhere else than the answer’s rect clears the answers(think of it as a back button to the question). And, to close the hint panel, we hit the esc button.

After fully implementing the hint system’s pipeline end to end and testing with the furnace case, I will now be porting rest of the cases too. And just like english, it has support for many different language like Spanish, Italian etc. adding whose support for the hint system I am considering in the longer run. See you in the next blog.

Thanks for reading 🙂

The Rebel Alliance Needs YOU! Rebel Assault I & II Ready for Testing

Rebel Assault II: The Hidden Empire screenshot

The ScummVM Team is pleased to announce that Star Wars: Rebel Assault and Star Wars: Rebel Assault II: The Hidden Empire are ready for public testing!

Strap into the cockpit as Rookie One and fly for the Rebel Alliance in a galaxy far, far away. The first game features many iconic battles of the movies including the famous trench run to destroy the Death Star, while the sequel picks up after the Battle of Endor, sending you into the Dreighton Nebula to uncover what the Empire is still hiding.

Released in 1993, Rebel Assault was LucasArts' first game distributed exclusively on CD-ROM and became one of the defining showcases of the format, packing full-motion video, rendered space battles and a full orchestral score onto a single disc. Rebel Assault II followed in 1995 and included the first new Star Wars live action video filmed since the movies..

Although a rail shooter game, Rebel Assault uses the SCUMM engine more commonly used for point-and-click adventure games and shares a large portion of its code with Full Throttle, another LucasArts game.

For this testing period, ScummVM supports Rebel Assault in its multiple DOS releases, the Macintosh release, the 3DO release, and the Sega CD releases. Rebel Assault II is supported in its DOS/Windows release in its many languages, including English, French, German, Italian, Spanish, Brazilian Portuguese, and Japanese. The ScummVM reimplementation also features improved controls for both games, eliminating the frustrating struggle to steer the ship and addressing one of the biggest criticisms of the original releases. Finally, the PlayStation version of Rebel Assault II is still under development and is not part of this testing round.

As usual, you will need the original game data files to play. To join the test, grab a recent daily development build, add your game directory to ScummVM, and play through the games. Please report any problems you encounter on our issue tracker.

May the Force be with you, Rookie One. The Alliance is counting on you.

Week 10

Last week ended with detection and extraction finally sorted, and a promise that testing begins. This week I made good on it — I sat down and actually played through Déjà Vu, mouse in hand, watching for anything that felt off. It turns out playing a game from start to finish is the best bug report you can write, and Déjà Vu handed me a nice little pile of them.

Cleaning up the Clean Up

The Special → Clean Up menu action, which is supposed to tidily arrange the items inside a window, was throwing them outside the window instead — leaving the item boxes looking empty. The items were never actually lost, it was a redraw bug: the code was mixing absolute screen coordinates with window-relative ones. Once both sides spoke the same coordinate system, Clean Up went back to doing exactly what its name promises.

Reading to the end

The output console at the bottom of the screen was only showing the last line of a longer message — the rest scrolled off before you could read it. A small padding overshoot in the auto-scroll was to blame. Now the whole message stays visible.

A watch while you wait

While the engine chewed on a command, the screen just sat there, making it look frozen. A classic Mac touch fixed this: I show the watch cursor while a command is being processed, so it’s clear the game is working and not stuck.

Animations at the right speed

This was my favourite one. Animations — the cab driver turning his head, the “BOOM” flash when you fire a gun — were blowing past almost instantly, when they should linger for a second or two. The culprit was the script SLEEP opcode, which computed its delay as (ticks / 60) * 1000. Because the division happened first, any pause shorter than a second was truncated straight to zero. Swapping it to (ticks * 1000) / 60 (and guarding against negative values that would otherwise wrap into an enormous delay) brought every animation back to its intended pace.

The ones that got away

Not everything landed. The elevator doors still animate too fast, and I spent a good while trying to page the console text like the original’s “Click to continue” prompt. Both run into the same wall: MacVenture only draws a fully consistent screen at the end of its main loop, and any attempt to draw mid-script trips over half-built windows and invalid rectangles. So for now those two stay on the list — the right fix needs the engine’s proper animation path, not a quick hack.

Next week

There are still a few small things left to polish in Déjà Vu, but it’s playable from start to finish now, which feels great. So it’s time to move on: next up I’m starting on Déjà Vu II, playing it through the same way and fixing whatever surfaces. I’ll also keep the elevator-door animation and the “Click to continue” console sync on the list, to come back to through the engine’s proper update path.

As always, thanks to my mentors for the steady guidance and patience — onward to Déjà Vu II!

Into the Light

Welcome back! This is week 9 of my GSoC project, and we’re very close to full DOS support for the Dungeon Master engine. Sound is working, graphics are mostly in place — the last remaining issue is corrupted wall graphics, which I’ve been digging into this week.

The Wall Atlas Problem

In the original Amiga version, the Left, Center, and Right walls for a given depth were stitched together into one wide “atlas” image — for example, D3LCR combined all three walls for Depth 3. When drawing, the engine would use an X-offset (srcX) to pick the right piece:

  • Left Wall (D3L): srcX = 0
  • Center Wall (D3C): srcX = 61
  • Right Wall (D3R): offset further right

When the game was ported to DOS, this atlas approach was abandoned. The Left and Right walls became their own standalone, tightly cropped images, and the D3LCR file was repurposed to contain only the Center wall.

This is where things get tricky. The engine’s wall-drawing logic was built around the Amiga’s atlas assumptions — offsets, widths, and memory layouts all baked in for that single-image approach. I’m currently working through exactly how to handle this, and hopefully in 1-2 days the dungeon walls will finally look the way they should.

I’ll try to add another detailed blog covering all the code changes by the end of this week. That’s it for this week — see you in the next one! 👋

You are here

Most of a Director game is one movie playing at a time. You leave one, you enter the next, and the engine only ever has a single score ticking over. But Director also lets a cast member be another whole movie (a “movie cast member”, #movie in Lingo) that runs in parallel, inside a sprite, while the host movie keeps going around it. This week I made ScummVM actually do that.

Director’s vocabulary, quickly: a movie is a timeline (its score) of frames. Each frame has numbered channels, and a sprite sitting in a channel is one on-screen instance of a cast member, an asset from the movie’s library. The stage is the window everything draws into, and Lingo is Director’s scripting language.

The thing that forced the issue is the mini-map in Star Trek: TNG Interactive Technical Manual. You walk the decks of the Enterprise-D in a QuickTime VR panorama, and in the corner there’s a little schematic of the deck with a marker showing where you are. That mini-map isn’t a picture the host draws: it’s a separate Director movie, linked in as a cast member, running its own scripts, watching a shared global to know where the marker goes, and setting another global to send you somewhere when you click it. Get movie cast members working and the mini-map comes alive. That was the goal.

By the time the mini-map worked I had three problems on my list that looked entirely unrelated, to each other and to it: the host’s caption text had stopped drawing, the input queue was growing without bound, and DT, the debugger, was flickering between two movies’ scores several times a second. I assumed three bugs. Underneath, they were one.

A cast member that drew nothing

Movie cast members and film loops share the exact same layout on disk. The only real difference is a single flag: a film loop is a canned animation, a movie cast member runs its own Lingo (scriptsEnabled). Because of that shared format, ScummVM already modelled MovieCastMember as a subclass of FilmLoopCastMember, and inherited the film loop’s loader, which looks for an embedded SCVW resource. A movie cast member doesn’t have one. Its content lives in a separate movie file, named in the cast info. So the loader found nothing, and the cast member drew nothing at all.

The first job, then, was a real load(): take the linked path out of the cast info, resolve it with findMoviePath, open the archive, and build a proper Movie out of it that the cast member owns. Point the inherited _score at that movie’s score and suddenly there are frames to draw.

There was also a bug. The moment the linked movie loaded, the whole stage resized itself to the mini-map’s dimensions and the panorama vanished. A movie, when it loads, assumes it owns the stage: it resizes the window, recentres it, repaints the background colour. That’s correct for a movie you open normally, and completely wrong for one living inside a sprite on someone else’s stage.

The fix is a flag, Movie::_isEmbedded, set on the linked movie. Anywhere loadArchive() reaches for the stage, it now checks that flag first and leaves it alone. An embedded movie borrows the host’s window; it never gets to own it. That flag turns out to be load-bearing: it comes back to guard rendering twice more before this is over.

Class diagram: MovieCastMember subclasses FilmLoopCastMember and owns a linked Movie whose _parentMovie points back at the host.
Where a movie cast member sits: it subclasses the film-loop cast member and owns a linked Movie, which points back at the host through _parentMovie.
Running in parallel

Here is the idea that organizes everything below, and it isn’t mine, sev handed it to me at the start: a movie cast member is a Window that doesn’t own a window. A real ScummVM Window has its own current movie, its own execution state, and its own step. A movie cast member needs all three, but it has to borrow them from the host, use them for exactly one step, and give them back. Every fix in this post is a consequence of getting that borrow-and-return right.

Start with the step. Loading a movie is not the same as running one. A film loop is a flipbook: you just ask it for the sprites at frame N. A movie cast member has to step: run its frame scripts, honour go, fire its events, advance. So update() now steps the linked movie’s score exactly like a real movie does.

The word “exactly” hides a decision: how fast does it step? A movie has its own tempo channel, so should the mini-map run at its own tempo, independent of the host? Rather than guess, I built a test movie in Director 4: a host at one tempo with an embedded movie authored at a different one, each incrementing a visible counter, and watched what the original engine did. The counters stayed locked together. Under D4, a movie cast member ignores its own tempo channel entirely and advances once per host frame, no independent clock, no drift. I’ve only verified this for D4; if D5 or D6 differ, that’s the first thing I’d re-check.

Then there’s the question of whose movie is “current”. Almost everything in Lingo resolves against the current movie: go, handler lookup, cast lookup, the clickOn. If the mini-map’s go is going to move the mini-map and not the panorama, the linked movie has to be the current movie while it steps. So update() swaps the window’s current movie to the linked one, steps, and swaps it back. For the duration of one step, the embedded movie is the current movie, and everything in Lingo resolves against it.

Reaching out, and getting on screen

Two things fell out of the borrow that I hadn’t planned for. First, the mini-map’s scripts call handlers that don’t exist in the mini-map: levelblinker lives in the host. And they read cast members that also live in the host. The linked movie isn’t self-contained; it’s written assuming it can reach up into whatever movie loaded it. That gave Movie a second new member, _parentMovie, and handler and cast lookups now fall back to it when the embedded movie comes up empty. The mini-map simply doesn’t run without it.

This fallback makes this title work, and it might be the wrong general answer: Director also has shared and external cast libraries, and a message hierarchy with a specified order, and it’s possible the “authentic” fix for another game is one of those rather than “fall back to whoever loaded me.” I verified that the mini-map needs the parent for levelblinker and its cast lookups; I have not verified that “fall back to the parent” is what Director itself does in general.

Per-frame flow: the host steps the linked movie, which refreshes its own channels, then the host composites them through getSubChannels().
One host frame: the host steps the linked movie, which refreshes its own channels; the host then pulls those channels into the sprite through getSubChannels().

Second, getting the embedded movie’s pixels onto the screen. Rendering in ScummVM’s Director engine is pull-based: a window renders the channels of its current movie and nothing else. The embedded movie is never the window’s real current movie (only for that flicker of a step), so it can’t push itself onto the stage. Instead the host pulls: the embedded movie’s own renderFrame() refreshes its channels but, seeing _isEmbedded, stops short of drawing them anywhere, and then getSubChannels() takes those live channels, scales them into the sprite’s bounding box, and composites them. “Live” is the important word: it’s the actual running channels, so anything the mini-map’s scripts change shows up immediately.

Ghost in the corner

With all that in place the mini-map appeared, animated, and tracked my position. And it also drew a second copy of itself, small and wrong, jammed into the top-left corner of the stage.

My first guess was a highlight artifact, something about the sprite’s hilite flag drawing where it shouldn’t, so I tried guarding that. It didn’t help, and I dropped it.

The real cause was the pull-based rule again, from the other side. When the embedded movie stepped and called something that triggered updateStage, it went all the way into Window::render() and rendered its own channels onto the shared window, at the embedded movie’s native origin, which is the top-left of the stage. The composite path was doing its job in the sprite; this was a second, illegitimate path drawing the same content in the wrong place.

The guard I added is at the top of Window::render(): if the current movie is embedded, return immediately. It’s a guard; I’m discarding a render request that should never have travelled this far, rather than modelling what updateStage “should” do for an embedded movie in real Director. For this engine the invariant I want holds either way, an embedded movie reaches the stage through exactly one door, getSubChannels(), and no other. Ghost gone.

Two other things were wrong by this point, and I was ignoring both. Mouse events were piling up in the queue instead of draining. DT was flickering between two scores. Neither seemed connected to anything I’d just done, so I wrote them down and kept going.

Text that wouldn’t draw

The mini-map worked, but a piece of the host broke: the panorama’s description text (the caption that tells you what you’re looking at) stopped drawing. It rendered fine on master. Something in my branch had killed it.

I couldn’t see it by reading, so I bisected (kind of). First across the commit series: build each commit, run it, look for the text. One commit was fine, the next was broken, which pinned it to “run the movie cast member as a parallel movie”. That’s a big commit, so I bisected within it, as a designed ladder of four builds, each enabling one more thing than the last:

  • don’t step the embedded movie at all, text draws;
  • start the embedded movie but don’t step it, text draws;
  • step it, but skip the sprite update, text draws;
  • step it with frame-script execution, text gone.

The last rung was the one that broke, so the culprit was specific: the embedded movie running its own frame scripts.

The invariant that was violated is small and exact. A Window owns one LingoState: a call stack, plus a stack of frozen states, which is how Director suspends a script mid-run to go do something else. That’s one thread of execution. The mini-map issues a go() on every single frame (that’s how it repositions the marker), and each go() freezes the current state and pushes it onto the window’s frozen stack. Because that stack was shared between the host and the embedded movie, the mini-map’s per-frame freeze kept pushing the host’s own scripts aside, and the script that draws the caption never got to run.

The fix is to stop sharing the one thing that must not be shared. The movie cast member now carries its own LingoState, and a new Window::swapLingoState() exchanges it onto the window only for the duration of a step, then swaps it back. The mini-map’s go() now freezes the mini-map’s own state and leaves the host’s completely alone.

Before: one shared LingoState, host scripts blocked. After: one state per movie, the host's untouched.
Before, one shared execution state, so the mini-map’s per-frame freeze blocked the host’s scripts. After, one state per movie, with the host’s left untouched.

Then the other two symptoms went quiet, and it took me a moment to see why, because they weren’t fixed by identical means.

The input queue drains only when the window isn’t mid-jump: in Score::step() the drain is gated on !hasJump and an empty frozen stack. With one shared frozen stack, the mini-map’s per-frame go() left the window permanently mid-jump, so the host’s routed mouse events had no frame in which they were allowed to drain. Isolating the state means the host is never mid-jump on the mini-map’s behalf; and because the mini-map is always mid-jump on its own behalf, update() drains its routed clicks explicitly, once per step. Same root cause, two coordinated fixes.

DT was flickering because it samples the window’s live Lingo state once per frame, and with a single shared state it had a real chance of sampling while the embedded movie owned it. The current-movie swap is still there, so DT can still land inside a step, but it now reads a state that belongs unambiguously to one movie instead of a half-updated shared one, and in practice the flicker is gone. I’d call this one strongly suspected rather than proven.

One shared LingoState; three symptoms.

Shared globals, private minds

It would be easy to conclude from all that the two movies should be walled off completely. They shouldn’t, and the mini-map is exactly why. Its whole job is a conversation with the host through shared globals: it reads gNodeNow to know where to put the marker, and writes gNewNode when you click to ask the host to travel. Wall the globals off and that conversation becomes impossible.

So the line to draw is between two things that get lumped together as “state”. Global variables stay shared: they live on Lingo, one table, host and embedded both reading and writing it; that’s the communication channel. Execution state (the call stack, the frozen stack, where each movie is in its own scripts) is private, one per movie.

Runtime objects: the Window owns one LingoState and swaps the embedded one in per step; Lingo's globalvars are shared.
Globals stay shared on Lingo; execution state is swapped per movie, one LingoState at a time on the window.
A click, end to end

The click is the whole design in one path, because it crosses the host/embedded boundary twice and the only things that cross with it are two globals. The diagram below traces all seven steps; the part worth saying in prose is the shape. Your click is hit-tested by the host, routed into the mini-map (with the bounding-box scaling inverted so the coordinates land in its own space), and handled by its on mouseUp as set gNewNode to the clickOn - 10 (the clickOn is the clicked sprite’s channel number; the - 10 is the mini-map’s own offset from channel to node id). From there it’s globals only: the host reads gNewNode, navigates, and writes gNodeNow; on its next exitFrame the mini-map reads gNodeNow and moves the marker. Two globals cross the boundary; everything else stays on its own side.

Seven-step click sequence, from the host's processInputEvent to the mini-map moving its marker.
A click crosses the boundary twice; only gNewNode and gNodeNow cross with it.
Loose ends

A handful of things I closed, verified, or explicitly left open:

  • scriptsEnabled. The flag this all started with was decoded but never enforced, so a movie cast member always ran its Lingo. It’s now honoured: with scripts off, the linked movie is a passive flipbook. The lever already existed as Score::_haveInteractivity, which gates every event, frame scripts included, while a frame still advances underneath it, so the movie’s frames and channels update but no Lingo runs and it doesn’t respond to the mouse.
  • Blast radius of the hit-test change. Sprite::respondsToMouse() now returns true for a movie cast member (when its scripts are enabled). That’s an engine-wide function, but the change only adds a branch for kCastMovie; every other cast type takes the same path as before, so no other game’s click behaviour moves.
  • Regressions. These changes touch shared render and hit-test paths, so the check that matters is the engine’s D4 unit-test suite: it still passes at its baseline (196 pass, 11 pre-existing failures), unchanged from master. But there are places tests don’t cover.
  • Untested corners. One movie cast member on stage, single instance, is what I ran. Two at once, and a movie cast member nested inside a linked movie, are both things Director allows and I haven’t tested; the state swap is a two-slot exchange, and a robust version is probably a push/pop. I also haven’t profiled the per-frame cost of stepping a second score, though with one embedded movie it isn’t perceptible.
  • Moving in the panorama doesn’t move the map. Clicking the map navigates the panorama, but walking through the panorama doesn’t move the marker. The map is a pure consumer of gNodeNow, and the host only updates gNodeNow from its QTVR node-change path, so this is a QTVR-side gap, not a moviecast member one.
Where this is headed

Next up is the DT side, so the next person to open a movie cast member can watch both scores at once. None of the debugging above was done with a nice tool: it was rebuild-and-observe and reading the call graph, and DT itself was one of the broken things. Which is exactly the gap I want to close, and why this line from Zig’s creator Andrew Kelley (link to the quote) is the note I want to end on:

I needed to code up a simulation, I needed some visualization, I needed more introspection, I needed a way to understand what is happening, so that debugging wasn’t an all-day affair where I was using command line tools, but debugging could look like just looking at an animated graphic of what’s happening and just spotting the obvious problem and fixing it immediately.

When you have the right simulation, that’s what you can do. When your system is accessible, bugs are trivial.

– Andrew Kelley

The code

Everything above is in the movie cast member PR: https://github.com/scummvm/scummvm/pull/7752

It’s seven commits, building from reading the flag word correctly, through loading and playing the linked movie, to running it in parallel and finally isolating the Lingo state.

 

Week 10

Welcome back, the CgBI decoder is up and loading the assets wonderfully, see-

I am currently rendering text on it which is too close to be completed too. The text is rendered like this

I have already ported the required infrastructure for the hint system that includes methods like setSeenScreen and hasSeenScreen which, as their names suggest, sets the screen to true and tells if the screen is set respectively. It tracks that using a boolean array _seenScreen. Similar methods and array are there for the hint answers too. The hint lookup table is almost completed too. You’ll see that in the next blog.

Once the hint system gets implemented, I will most likely be moving to the implementation of the intro and outro videos. After that, I’ll implement the audio.

There’s hotspost too which is yet to be implemented. So, I’ll have to do that too. See you next week.

Thanks for reading 🙂

WEEK 9

After last week’s big push to get Chamber of the Sci-Mutant Priestess over the finish line, this week was all about MacVenture — turning last week’s “the games boot!” moment into something people can actually install and play.

Getting the games out of Steam

The biggest chunk of work went into the tooling. Last week I explained the awkward situation with the Steam releases: the actual game data isn’t sitting there as loose files, it’s a Mac HFS disk image buried inside a Chromium wrapper executable. Extracting that by hand once, for debugging, is one thing — but nobody buying the games on Steam should have to do that.

So I taught ScummVM’s dumper-companion how to do it. Point it at the Steam .exe, and it now digs the HFS disk image out of the PE resource, punycodes the filenames so they survive on a non-Mac filesystem, and hands you a clean set of game data. What used to be a fragile manual ritual is now a single command.

The Apple IIGS rabbit hole

While I was in there, I noticed the Steam executables actually carry two disk images — the Mac one everybody expects, and an Apple IIGS version tucked away in a separate resource. The IIGS releases have their own look and feel, so it felt wrong to leave them on the table.

Two problems stood in the way. First, the dumper only knew about the Mac disk, so I extended it to pull the IIGS .2mg image as well. Second — and this was the more surprising one — ScummVM’s detection for the IIGS games was quietly dead. The detection entries existed, but there was no data-fork fallback, so the files these games actually ship with never matched anything. I fixed the detection so it hashes the right fork, and suddenly the IIGS versions light up in the launcher like they always should have.

A detour back to Chamber

One small but satisfying fix landed on the Chamber side too. A user reported (bug #17004) that if you had both the CGA and EGA data in the same folder, the engine would stubbornly force CGA regardless of what you picked. It turned out to be a detection-hint issue, and now the engine honors the variant you actually chose.

Testing begins

With the tooling in place, I’ve started the part I’ve been looking forward to: real playthroughs. Déjà Vu is first up — the noir detective one, the natural place to start — and so far it’s going really well. The window manager behaves, items drag and drop where they should, the text scrolls cleanly. No showstoppers yet, which is exactly what you want to see from the first of four games.

Next week

The plan is to keep grinding through the playthroughs — finish Déjà Vu, then move on to Déjà Vu II, Uninvited, and Shadowgate — noting anything that misbehaves along the way. If they hold up like Déjà Vu has, the goal is to get MacVenture’s Steam and IIGS releases marked as testing so players can start reporting back.

As always, thanks to my mentors for the steady guidance — onward to the testing grind!