PSU lives on the Clementine server. Support the people who run it. Support Clementine on Patreon (opens in a new tab)

← Dev blog

The title screen, with no Wine

The game's title screen now renders on my Mac with no Wine anywhere. It is slow, there is no input and no audio, and I have not yet confirmed what shows up in the live window. I will get to all of that. First the usual caveat. This is a private educational proof of concept. Nothing public ships without approval from the Clementine staff, and I am not affiliated with SEGA or the Clementine team.

What ran

The x86 interpreter from the last post now drives the whole startup. The C runtime initialises, the game's own init code runs, and control reaches WinMain after about 110,000 interpreted instructions. Along the way the game made 685 calls into Windows, spread over 55 distinct imports. That is a small surface, which is why a Win32 core of roughly 150 handlers was enough to get through it.

The sandbox matters here, since the rules say I never touch a player's install. Every write the game attempts lands in a scratch copy. I tested that with a read-only original and twelve different spellings of a path escape. None of them got out. Networking is denied and logged, and nothing connected to any server.

Getting a window and a device

With the window and Direct3D wiring in place, the game ran for 344 frames in 31 seconds and made 122,303 Direct3D 9 calls. It used 46 distinct methods. The heavy ones were SetRenderState at 27,512 calls, DrawPrimitiveUP at 24,918 and SetTexture at 14,978. The bridge from the earlier triangle spike (built on an open source Direct3D 9 to Metal layer, not Wine) already covered almost everything the game asked for. Four adapter and capability queries were missing and I added them. No call came back as not implemented.

At the end of a run I read the back buffer out and checked it. 300,588 of its 307,200 pixels were non-black, which is the title screen. I am not going to describe or show the artwork, just say that the number is what a drawn 640 by 480 frame looks like and a blank one does not.

Why it is only 8 to 11 frames a second

That is the speed of the interpreter, and nothing else. It decodes every instruction as it goes. The game's own Sleep(16) pacing is kept as written, and I will not uncap the frame rate, because the Clementine rules say not to. The way this gets to 60 is the lifter, which turns the game's functions into C that I compile on the player's machine. So far 70.6% of functions lift. The interpreter is the fallback for the rest, and also the thing I check the lifted code against.

Three bugs

Stub addresses collided with the run sentinel. The interpreter stops when execution reaches a special sentinel address. The bridge hands out fake addresses for the Direct3D methods the game calls, and its range overlapped the sentinel and the markers I use for exception handling. I moved the bridge's range, and its heap, out of the way.

The back buffer had no format. The game asks for a 640 by 480 mode. The bridge listed the Mac display's single mode, and that is not 640 by 480. The game found no matching mode, so its back-buffer format stayed uninitialised and its init gave up. The fix was to report a Windows-style list of display modes instead.

The scheduler ignored priorities. The game starts a few threads, pins them all to one CPU and relies on priority order to decide who runs first. My scheduler treated them all alike, and a suspend on the calling thread did not stop it. About 3 runs in 10 crashed. Once the scheduler honoured priority and the suspend, 0 of 12 runs crashed. Twelve is not a large sample, so I will keep running it.

Two detours

The game's strings are encrypted in the file, so a plain string search over the binary found nothing useful. I had to learn what it was doing from the calls it made instead.

Early on I found a file called npgl.erl in a GameGuard folder and spent too long treating it as part of the anti-cheat. It is the game's own log file.

What is not true yet

It is slow. There is no input and no audio. The live window is the part I have not verified. A screen capture of it came out black, and I think that is the capture path and not the window, since the back-buffer readback is correct. I would rather say "unverified" than guess.

Next

The lifter's coverage is the main job, since that is the road to 60 frames a second. After that comes input, then sound. Each of those will get its own post when it works.