Skip to main content

The ever growing window

One issue with the EVE client running under Wine is that when starting in windowed mode, the window would grow by a few pixels every time. I spent some time trying to figure this out and wanted to document my findings here before forgetting all the details.
Rather than digging directly into Wine source code I started by looking at how the window is created in our code. That code is a bit convoluted, to say the least, and largely hasn't been touched in years. But hey, it works, right, and if it ain't broken don't fix it.
There are two parts to the story when creating the window - there is the window itself, and the Direct3D backbuffer that is drawn in the window. When the window is resized, the backbuffer is recreated to fit the window. The Python code calls trinity.app.ChangeDevice, which internally creates the window if it doesn't already exist, then creates a backbuffer of the appropriate size.
The window proc for the window handles the WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE messages and calls a Python callback on WM_EXITSIZEMOVE to notify of a size change in the window. This callback ends up calling ChangeDevice, which in turn recreates the backbuffer with the new size.
While going through the code I noticed that the window is initially created with a zero size, then later resized to the size stored in the settings. I couldn't help finding this a bit suspicious and decided to try to change that, to create the window with some minimal size rather than the zero size. Running the client under Wine after this change fixed the issue - the window no longer changed size.

But why?

I wasn't really content with this fix, as Wine is supposed to behave just like Windows and this was obviously different behavior. Wine is a large codebase and debugging it is a daunting task, especially for a newcomer such as myself. Trying to debug it through the EVE client seemed to be bordering on madness so I decided to see if I could test the theory that it was indeed the zero size window that was the key. I wrote a simple stand-alone program that created two windows - one zero sized, and one with a proper size and position. I set the window proc to simply print out all messages received so I could compare the messages received for each window.
When running the resulting executable on Windows, the messages were essentially the same for both windows. Running the same executable on Mac under Wine, the zero sized window received extra messages, including WM_ENTERSIZEMOVE and WM_EXITSIZEMOVE. Theory confirmed - Wine behaved differently when creating a zero sized window.

Fixing Wine?

Having a simple repro case I figured it would be straight-forward to track down where this extra resize came from. Wine has a powerful logging mechanism and most functions output useful information if you enable the channel they log to. The information I needed was in the 'win' and 'macdrv' channels so I set the WINEDEBUG environment variable appropriately:
export WINEDEBUG=+win,+macdrv
The log output confirmed that the window was created with the right values - the resizing didn't happen until the window was activated and rendered for the first time. The problem wasn't in the implementation of CreateWindowEx function or any of the functions it called. After some digging I found that low level functions in winemac.drv were checking for empty areas, ensuring that rects had a height and width of at least one.
The problem was, though, that commenting that code out resulted in the zero sized window not showing up at all. It seems that Cocoa does not like zero sized windows. Note that the zero size refers to the client area - the size of the window is always larger to account for the window frame. On Windows, a zero sized window would show as a minimal size frame, with just enough room for the system menu in the top left corner and the minimize, maximize and close boxes on the right. On the Mac, under Wine, the frame is smaller, with the close, minimize and maximize buttons on the left, but passing a zero size to Cocoa would not even render that minimal frame. This explains those checks in the low level code.

A workaround?

Fixing the low level functions in Wine didn't prove to be feasible so I tried a different approach. Note that I was looking for an alternative to the fix I'd already found in the EVE client code. The reason for wanting that was twofold - one was that I'd simply checked that fix into MAINLINE without paying attention to when that would go out, and now I wanted to get a fix out earlier than the regular EVE release cadence would allow me. The other reason is that I wanted to use this as an opportunity to get some experience in debugging Wine issues.
The answer was to ensure that the window wouldn't get those extra WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE messages. That meant ensuring that Wine and Cocoa would agree from the start on what the size of the window was supposed to be. The problem was that the window was created with a given size (that resulted in a zero sized client area), then when the Cocoa window was created, it got enlarged by one pixel.
So, I tried the following messy hack in dlls/user32/win.c (in WIN_CreateWindowEx):
 ...
 RECT emptyRect;
 SetRect(&emptyRect, 0, 0, 0, 0);
 AdjustWindowRect(&emptyRect, cs->style, FALSE);
 if((cs->x == emptyRect.left) && (cs->y == emptyRect.top) && (cs->cx == emptyRect.right - emptyRect.left) && (cs->cy == emptyRect.bottom - emptyRect.top))
 {
   // This is a temporary workaround for EVE creating its window with zero size initially.
   TRACE("Changing values for empty rect");
   cs->x = 40;
   cs->y = 40;
   cs->cx += 1;
   cs->cy += 1;
 }
 ...
To cut a long story short, this worked - the window was now created with it's zero size client area as far as the EVE client knows, but it was actually created safely on-screen, with a client area of one pixel. This meant the Cocoa window was created without any changes, thus not triggering any resize.

Now what?

I published a build of Wine with this fix to allow EVE players on Mac using Wine to enjoy a stable window size when starting up the client. I won't attempt to submit this as a fix to the Wine repo - I've no clue what the side effects might be for generic Windows applications. It's probably somewhat of an edge case to create windows with a zero size client area so a better fix is to change our client code as I did. Once that fix makes it out to Tranquility I'll remove that hack mentioned above, but this proved to be a valuable exercise in debugging Wine issues.

Comments

Popular posts from this blog

Working with Xmpp in Python

Xmpp is an open standard for messaging and presence, used for instant messaging systems. It is also used for chat systems in several games, most notably League of Legends made by Riot Games. Xmpp is an xml based protocol. Normally you work with xml documents - with Xmpp you work with a stream of xml elements, or stanzas - see https://tools.ietf.org/html/rfc3920 for the full definitions of these concepts. This has some implications on how best to work with the xml. To experiment with Xmpp, let's start by installing a chat server based on Xmpp and start interacting with it. For my purposes I've chosen Prosody - it's nice and simple to install, especially on macOS with Homebrew : brew tap prosody/prosody brew install prosody Start the server with prosodyctl - you may need to edit the configuration file (/usr/local/etc/prosody/prosody.cfg.lua on the Mac), adding entries for prosody_user and pidfile. Once the server is up and running we can start poking at it...

Simple JSON parsing in Erlang

I've been playing around with Erlang . It's an interesting programming language - it forces you to think somewhat differently about how to solve problems. It's all about pattern matching and recursion, so it takes bit getting used to before you can follow the flow in an Erlang program. Back in college I did some projects with Prolog  so some of the concepts in Erlang were vaguely familiar. Supposedly, Erlang's main strength is support for concurrency. I haven't gotten that far in my experiments but wanted to start somewhere with writing actual code. OTP - the Erlang standard library doesn't have support for JSON so I wanted to see if I could parse a simple JSON representation into a dictionary object. The code is available on Github:  https://github.com/snorristurluson/erl-simple-json This is still very much a work in progress, but the  parse_simple_json/1 now handles a string like {"ExpiresOn":"2017-09-28T15:19:13", "Scopes":...

JumperBot

In a  previous blog  I described a simple echo bot, that echoes back anything you say to it. This time I will talk about a bot that generates traffic for the chat server, that can be used for load-testing both the chat server as well as any chat clients connected to it. I've dubbed it  JumperBot  - it jumps between chat rooms, saying a few random phrases in each room, then jumping to the next one. This bot builds on the same framework as the  EchoBot  - refer to the previous blog if you are interested in the details. The source lives on GitHub:  https://github.com/snorristurluson/xmpp-chatbot Configure the server In an  earlier blog  I described the setup of Prosody as the chat server to run against. Before we can connect bots to the server we have to make sure they can log in, either by creating accounts for them: prosodyctl register jumperbot_0 localhost jumperbot prosodyctl register jumperbot_1 localhost jumperbot ... or by ...