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.

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_…

Waiting for an answer

I want to describe my first iteration of exsim, the core server for the large scale simulation I described in my last blog post. A Listener module opens a socket for listening to incoming connections. Once a connection is made, a process is spawned for handling the login and the listener continues listening for new connections. Once logged in, a Player is created, and a Solarsystem is started (if it hasn't already). The solar system also starts a PhysicsProxy, and the player starts a Ship. These are all GenServer processes. The source for this is up on GitHub: https://github.com/snorristurluson/exsim Player The player takes ownership of the TCP connection and handles communication with the game client (or bot). Incoming messages are parsed in handle_info/2 and handled by the player or routed to the ship, as appropriate. The player creates the ship in its init/1 function. The state for the player holds the ship and the name of the player. Ship The ship holds the state of the ship - …

Replicated Mnesia

I'm still working on my expiring records module in Erlang (see here and here for my previous posts on this). Previously, I had started using Mnesia, but only a RAM based table. I've now switched it over to a replicated disc based table. That was easy enough, but it took a while to figure out how to do, nonetheless. I had assumed that simply adding ... {disc_copies, [node()]} ... to the arguments to mnesia:create_table would be enough. This resulted in an error: {app_test,init_per_testcase, {{badmatch, {aborted, {bad_type,expiring_records,disc_copies,nonode@nohost}}}, ... After some head-scratching and lots of Googling I realized that I was missing a call to mnesia:create_schema to allow it to create disc based tables. My tests for this module are done with common_test so I set up a per suite initialization function like this: init_per_suite(Config) ->mnesia:create_schema([node()]), mnesia:start(…