Monday, September 21, 2020

Fixing an obscure Undertale bug

Undertale's 5th anniversary was a couple of weeks ago, so I decided to play through it again. However, when I tried to start it up, it just crashed right away. There was a lot of diagnostic output, but no error messages, just an exit. These kinds of bugs are difficult to diagnose, but I wasn't going to let that stop me from enjoying those goat hugs.

I ran Steam in a console window, and changed the launch options to run the game under a debugger:

That at least told me what the error was: SIGFPE - usually caused by an integer division by zero.

Reading symbols from /media/disk1/lstuff/steam/steamapps/common/Undertale/runner...(no debugging symbols found)...done.
(gdb) r
Starting program: /media/disk1/lstuff/steam/steamapps/common/Undertale/runner 
.
.
.
Thread 1 "runner" received signal SIGFPE, Arithmetic exception.
0x081d7332 in ?? ()
(gdb) 

What could it be trying to divide by zero? Maybe the rest of the output might give me a hint.

***************************************
*     YoYo Games Linux Runner V1.3    *
***************************************
CommandLine: -game game.unx
ExeName= /media/disk1/lstuff/steam/steamapps/common/Undertale/runner
MemoryManager allocated: 4031222
INI DisplayName=UNDERTALE
SavePrePend /home/ktpanda/.config/UNDERTALE/
GAMEPAD: Initialising Ubuntu support
Attempting to set gamepadcount to 4
display=0x9682cd0
Display Size(Pixels): 1920,1080
CreateDesktopWindow 640,480
Win #1
XF86VidModeExtension-Version 2.2
Got Doublebuffered Visual!
glX-Version 1.4
Icon: w=64 h=64
Creating window of width 640, height 480
sw=0 wh=0
WindowCentre: 640,300
Depth 24
Congrats, you have Direct Rendering!
sync = 1
**** GLX Extensions ***
GLX_EXT_visual_info GLX_EXT_visual_rating ...
Checking for GLX_EXT_swap_control
Vsync: GLX_EXT
DOUBLE BUFFERED
OpenGL: version string 4.6.0 NVIDIA 440.66.12
OpenGL: vendor string NVIDIA Corporation
OpenGL GLSL: version string 4.60 NVIDIA
Extensions:
GL_AMD_multi_draw_indirect GL_AMD_seamless_cubemap_per_texture ...
Anisotropic filtering supported, max aniso 16
This is where it would have set them fullscreen= 0, they are 0,0    displaywidth/h 0,0
Texture #1 16,16
Texture #2 16,16
Texture #1 16,16
Texture #2 16,16
finished(2)!!
Texture #1 1,1
Texture #2 1,1
finished(2)!!
Total memory used = 81515771(0x04dbd4fb) bytes

So one thing that stands out is the line that says displaywidth/h 0,0. If for some reason it was seeing the display resolution as 0x0, it is very plausible that trying to calculate something related to the aspect ratio would result in division by zero. But where is it getting that size from? Especially since earlier, it detects it correctly: Display Size(Pixels): 1920,1080.

Time to fire up xtrace. This utility intercepts all communication between the program and the X display server and dumps it. Undertale must be calling some weird function and misinterpreting the results.

***************************************
*     YoYo Games Linux Runner V1.3    *
***************************************
CommandLine: -game game.unx
ExeName= /media/disk1/lstuff/steam/steamapps/common/Undertale/runner
MemoryManager allocated: 4031222
INI DisplayName=UNDERTALE
SavePrePend /home/ktpanda/.config/UNDERTALE/
GAMEPAD: Initialising Ubuntu support
Attempting to set gamepadcount to 4
000:<: am lsb-first want 11:0 authorising with 'MIT-MAGIC-COOKIE-1' of length 16
000:>: Success, version is 11:0 vendor='The X.Org Foundation' release=11906000 resource-id=0x0a200000 resource-mask=0x001fffff [...] width[pixel]=1920 height[pixel]=1080 [...]
000:<:0001: 20: Request(98): QueryExtension name='BIG-REQUESTS'
000:>:0001:32: Reply to QueryExtension: present=true(0x01) major-opcode=133 first-event=0 first-error=0
000:<:0002:  4: BIG-REQUESTS-Request(133,0): Enable
000:>:0002:32: Reply to Enable: maximum-request-length=4194303
000:<:0003: 20: Request(55): CreateGC cid=0x0a200000 drawable=0x00000245 values={background=0x00ffffff}
000:<:0004: 24: Request(20): GetProperty delete=false(0x00) window=0x00000245 property=0x17("RESOURCE_MANAGER") type=0x1f("STRING") long-offset=0x00000000 long-length=0x05f5e100
000:>:0004:2168: Reply to GetProperty: type=0x1f("STRING") bytes-after=0x00000000 data='[...]'
000:<:0005: 20: Request(98): QueryExtension name='XKEYBOARD'
000:>:0005:32: Reply to QueryExtension: present=true(0x01) major-opcode=135 first-event=85 first-error=137
000:<:0006:  8: XKEYBOARD-Request(135,0): UseExtension major=1 minor=0
000:>:0006:32: Reply to UseExtension: major=1 minor=0
display=0x95bacd0
Display Size(Pixels): 1920,1080
CreateDesktopWindow 640,480
Win #1
.
.
.
000:<:003a: 16: Request(98): QueryExtension name='RANDR'
000:>:003a:32: Reply to QueryExtension: present=true(0x01) major-opcode=140 first-event=89 first-error=147
000:<:003b: 12: RANDR-Request(140,0): QueryVersion major-version=1 minor-version=5
000:<:003c:  8: RANDR-Request(140,8): GetScreenResources window=0x0a200003
000:>:003b:32: Reply to QueryVersion: major-version=1 minor-version=5
000:>:003c:3680: Reply to GetScreenResources: timestamp=0x42ae9f12 config-timestamp=0x0003cf23 [...]
000:<:003d: 12: RANDR-Request(140,20): GetCrtcInfo crtc=0x0000023a config-timestamp=0x0003cf23
000:>:003d:44: Reply to GetCrtcInfo: status=Success(0x00) timestamp=0x42ae9f12 x=0 y=0 width=0 height=0 mode=0x00000000 current rr=0 possible rr=Rotate_0,Rotate_90,Rotate_180,Rotate_270,Reflect_X,Reflect_Y outputs=; possible outputs=0x0000023e,0x0000023f,0x00000240;
sw=0 wh=0
WindowCentre: 640,300
Depth 24

Well, would you look at that!? GetCrtcInfo returns 0 for both width and height. This is an XRandR function, so its actual name is XRRGetCrtcInfo. This function is so obscure that searching for it brings up a few examples, some StackOverflow questions, and the header file where it's defined, but no actual documentation for it. I played around with the xrandr utility, hoping to figure out what's causing it to return zero, but nothing worked.

But it's still just a hypothesis that this one call is what's causing the crash. If I could just trick the game into thinking that it returned the correct size, then maybe it will work. The definition of XRRGetCrtcInfo looks like this:

typedef struct _XRRCrtcInfo {
    Time	    timestamp;
    int		    x, y;
    unsigned int    width, height;
    RRMode	    mode;
    Rotation	    rotation;
    int		    noutput;
    RROutput	    *outputs;
    Rotation	    rotations;
    int		    npossible;
    RROutput	    *possible;
} XRRCrtcInfo;

XRRCrtcInfo *
XRRGetCrtcInfo (Display *dpy, XRRScreenResources *resources, RRCrtc crtc);

I can use the debugger and break on the call to XRRGetCrtcInfo, then step out, then poke values into the structure it returns:

Reading symbols from /media/disk1/lstuff/steam/steamapps/common/Undertale/runner...(no debugging symbols found)...done.
(gdb)
(gdb) break XRRGetCrtcInfo
Breakpoint 1 at 0x804f4a0
(gdb) r
Starting program: /media/disk1/lstuff/steam/steamapps/common/Undertale/runner
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

***************************************
*     YoYo Games Linux Runner V1.3    *
***************************************
CommandLine: -game game.unx
ExeName= /media/disk1/lstuff/steam/steamapps/common/Undertale/runner
MemoryManager allocated: 4031222
INI DisplayName=UNDERTALE
SavePrePend /home/ktpanda/.config/UNDERTALE/
GAMEPAD: Initialising Ubuntu support
Attempting to set gamepadcount to 4
display=0x880ccd0
Display Size(Pixels): 1920,1080
CreateDesktopWindow 640,480
Win #1
XF86VidModeExtension-Version 2.2
Got Doublebuffered Visual!
glX-Version 1.4
Icon: w=64 h=64
Creating window of width 640, height 480

Breakpoint 1, 0xf7829d50 in XRRGetCrtcInfo () from /usr/lib/i386-linux-gnu/libXrandr.so.2
(gdb) finish
Run till exit from #0  0xf7829d50 in XRRGetCrtcInfo () from /usr/lib/i386-linux-gnu/libXrandr.so.2
0x082fc8f7 in ?? ()
(gdb) info registers
eax            0x888ccd0	143183056
ecx            0x0	0
edx            0x89606e8	144049896
ebx            0x873fae4	141818596
esp            0xffffa8a0	0xffffa8a0
ebp            0xf709db00	0xf709db00
esi            0x1002	4098
edi            0x895c170	144032112
eip            0x82fc8f7	0x82fc8f7
eflags         0x200282	[ SF IF ID ]
cs             0x23	35
ss             0x2b	43
ds             0x2b	43
es             0x2b	43
fs             0x0	0
gs             0x63	99
(gdb) p ((unsigned int*)0x888ccd0)[0]
$1 = 1133426070
(gdb) p ((unsigned int*)0x888ccd0)[1]
$2 = 0
(gdb) p ((unsigned int*)0x888ccd0)[2]
$3 = 0
(gdb) p ((unsigned int*)0x888ccd0)[3]
$4 = 0
(gdb) p (((unsigned int*)0x888ccd0)[3] = 1920)
$5 = 1920
(gdb) p (((unsigned int*)0x888ccd0)[4] = 1080)
$6 = 1080
(gdb) c
Continuing.
sw=1920 wh=1080
WindowCentre: 640,300
Depth 24

Success! It sees the proper display size now, and the game starts up!

But that's a lot of work I'd have to do every time I launch the game, especially given what happens near the end. Fortunately, Linux and some other Unix-like operating systems support a feature called LD_PRELOAD, which allows injecting a library into a program before it loads, allowing it to override functions in other libraries. I created a simple C library that I could use with LD_PRELOAD for an automated fix. I'm currently working on cleaning up the code to upload it.

Let me know if you see something similar! I couldn't find anyone else posting about this bug, so it must not be common.