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

Mnesia queries

I've added search and trim to my expiring records module in Erlang. This started out as an in-memory key/value store, that I then migrated over to using Mnesia and eventually to a replicated Mnesia table. The fetch/1 function is already doing a simple query, with match_object. Result=mnesia:match_object(expiring_records, #record{key=Key, value='_', expires_at='_'}, read) The three parameters there are the name of the table - expiring_records, the matching pattern and the lock type (read lock). The fetch/1 function looks up the key as it was added to the table with store/3. If the key is a tuple, we can also do a partial match: Result=mnesia:match_object(expiring_records, #record{key= {'_', "bongo"}, value='_', expires_at='_'}, read) I've added a search/1 function the module that takes in a matching pattern and returns a list of items where the key matches the pattern. Here's the test for the search/1 function: search_partial_…

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 to get a feel for h…

Expiring records in Erlang

I'm continuing my experiments with Erlang - this time trying out gen_server with a simple key/value store with a twist - the values have an expiration date. As a first iteration I'm simply using a dictionary to store the values, and only expiring records when they are looked up. My plan is to extend this later on so that this can be a global key/value store across multiple Erlang nodes but for now I'm focusing on two things - get something going using gen_server, and try out the common_test testing framework. The code is here: https://github.com/snorristurluson/erl-expiring-records Let's first take a look at a couple of the test functions, to show the usage of this: get_non_expired_record(Config) ->Pid=?config(pid, Config), Record= {"bingo", "bongo", erlang:system_time(second) +3600}, ok=gen_server:call(Pid, {add, Record}), {ok, "bongo"} =gen_server:call(Pid, {fetch, "bingo"}). get_expired_record(Config) ->Pid=…