Diablo is defeated ....

gho
2014-07-12
2014-07-25
  • gho

    gho - 2014-07-12

    This is a preview of next release, where DxWnd finally defeats a rendering problem in Diablo menu screens.
    Life sometimes is funny: I started maintaining DxWnd with some old but famous glories, and Diablo was one of the first.... but it soon showed this problem that resisted to investigations and fix attempts since now.
    The problem is that if you run diablo windowized in a size different from its native resolution (640 x 480) you can easily get distorted animations (see picture diablo.84).
    Why that happens? In emulation mode, the rendering is made agains a virtual surface that has always the native size, no matter how you stretch the real window, so what's the matter with the window size? It shouldn't care....
    Here comes the reason:
    In Diablo, the rendering in menu screens is made by library calls that lay the pixels on the child windows of the form, according to the size returned by the GetClientRect API. The problem is that the child windows are scaled, and when the program queries their size , DxWnd tries to recover the virtual size reverting the scaling operation.
    Here comes the problem: in integer arithmetic, if you have a number w (width of the child form) and you stretch it, you get w' = (w * 800 ) / 640,
    then you revert this operation and yuu get w'' = (w' * 640) * 800
    Is w'' equal to w ? Actually not always. The integer division is not precise, there is a reminder that has to be eliminated, and w'' is different from w, often by a single pixel, but this is enough to produce the diagonal effect.
    Since I cannot change math, the only solution is to store somewhere (into a sort of stack) the sizes of all child windows and to retrieve the exact value when needed.
    The screenshot diablo.85 is what you'll see from release v2.02.85 on, even though not all problems are fixed: another tough one is the fact that child window in windowed mode have a locked surface that inhibit blit operation such as showing a textured cursor.
    Well, it's something to keep your mind busy!...

     
    Last edit: gho 2014-07-12
  • aqrit

    aqrit - 2014-07-13

    Since I cannot change math

    Why not?
    Cast it up to an __int64 to carry an extra 32 bits of precision.
    After calculating, add 1 to the high bit of the low dword to round to the nearest whole number.

    // pseudocode
    unsigned __int64 v = (__int64) width;
    v <<= 32;
    //todo: calc
    v += 0x0000000080000000UL; // round
    v >>= 32; // truncate back to 32-bits
    

    here is a snippet from one of my window mode hacks for a 640x480 game that uses 11 extra bits of precision...

    // ClientToGame
    if( width <= 3276 ){
        // multiply at the start then divide at the end (1310720 == 640 << 11 == 640 * 2048)
        // we add 1024 (0.5 * 2048) to round so when we truncate we will have the nearest whole number 
        lpPoint->x = ( ( ( 1310720 / width ) * lpPoint->x ) + 1024) >> 0xB; 
    }
    
     
    Last edit: aqrit 2014-07-13
    • gho

      gho - 2014-07-13

      Uhm.... maybe it could work when scaling up (es. 640 x 480 -> 800 x 600) but I think it may fail when scaling down. In that case, the operation could be not reversible (that is, there could be different sizes that are transformed to the same smaller value, then how could you possibly recover the original one?).
      But it's a good idea, scaling up is the more frequent option: I'll see if it's possible to generalize.
      Since you wrote, can I ask you a favour?
      A problem with scaling down Diablo is that the graphic gets clipped: try it ad 400 x 300 and here is what you see. Likely, I failed scaling something, or maybe I did it twice. Any hint?

       
  • aqrit

    aqrit - 2014-07-13

    Without knowing the original window sizes you can't recover. I guess I wasn't thinking since all the games I've played with only had one window.


    Are there clippers attached/used for the child windows?
    I'll take a closer look when I have time ( probably Wednesday )

    edit: fixed a WTF with the text size.

     
    Last edit: aqrit 2014-07-13
  • gho

    gho - 2014-07-13

    In the meanwhile, I must admit you were right: I changed all formulas like these ones

    newx = (x * neww) / oldw;
    newy = (y * newh) / oldh;
    oldx = (newx * oldw) / neww;
    oldy = (newy * oldh) / newh;

    adding half an integer to round the division instead of truncate:

    newx = ((x * neww) + (oldw >> 1)) / oldw;
    newy = ((y * newh) + (oldh >> 1) / oldh;
    oldx = ((newx * oldw) + (neww >> 1)) / neww;
    oldy = (newy * oldh) + (newh >> 1)) / newh;

    And Diablo recovers the original exact size with no need for nasty memory structures to hold who knows how many records.
    I tested it for several bigger scaling. Of course, I still think it may fail for downscaled windows, but it's not such a common practice!

    edit

    In fact, Diablo works fine (forgetting the clipping...) at size 300 x 200, it does not at 301 x 200 !! But who cares?

     
    Last edit: gho 2014-07-13
  • gho

    gho - 2014-07-14

    Unfortunately, the problem doesn't show only when downscaling!
    There's a wrong ratio in the calculation of the graphic surface to process, and this processing is made directly in memory on the locked memory buffer of the backbuffer surface.
    So, what happens is that when you upscale, even if the image is not cropped or enlarged, the graphic routine tries to access an area wider than available, and this is causing a game crash when you scale too much. On my PC, for instance, I can't use the fake fullscreen mode, and the only workaround that I got so far is doubling the primary and backbuffer surface area.
    So, Aqrit, you're warmly invited to help about the clipping problem.

     
  • aqrit

    aqrit - 2014-07-17

    Diablo/Hellfire seems to get menu drawing dimensions from GetUpdateRgn.

    A window might have an update region, window region, and clip region.
    When a window is resized, it effects the sizes of these regions...
    and there are a lot of these functions and they're interconnected in less than amusing ways :(

    so lets say for example, Hellfire was set to run in a 320x480 window using DxWnd.
    I might get the main menu seemingly fixed like so:

    int __stdcall GetUpdateRgn_hookproc( HWND hWnd, HRGN hRgn, BOOL bErase ){
        int regionType;
        RECT rc;
    
        regionType = GetUpdateRgn( hWnd, hRgn, bErase ); // call real function
        if( regionType == SIMPLEREGION ){
            regionType = GetRgnBox( hRgn, &rc );
            if( regionType == SIMPLEREGION ){
                rc.right *= 2; // assumes scaled width is half the original*
                if( SetRectRgn( hRgn, rc.left, rc.top, rc.right, rc.bottom ) ){
                    ; // success
                }
            }
        }
        return regionType;
    }
    
     
    Last edit: aqrit 2014-07-17
    • gho

      gho - 2014-07-17

      aqrit, you're wonderful!

      I suspected the GetUpdateRgn was involved, but when I saw that all API parameters were for input only, I thought there was no way to alter the affected region.
      Selecting a new, wider region for me is a flash of inspiration, and a real strike of genius.
      To make exact computations DxWnd already has everything necessary, so the final and generalized code wasn't touched that much: here it is...

      int WINAPI extGetUpdateRgn(HWND hWnd, HRGN hRgn, BOOL bErase)
      {
          int regionType;
          RECT rc;
          regionType=(*pGetUpdateRgn)(hWnd, hRgn, bErase);
          if( regionType == SIMPLEREGION ){
              regionType = GetRgnBox( hRgn, &rc );
              if( regionType == SIMPLEREGION ){
                  dxw.UnmapClient(&rc);
                  if( SetRectRgn( hRgn, rc.left, rc.top, rc.right, rc.bottom ) ){
                      ; // success
                  }
              }
          }
          return regionType;
      }
      

      And I'm glad to announce that thank to this region correction, both the reduced and the enlarged situations went ok (see screenshots): the reduced correctly updates the whole screen, the enlarged crashes no more!

      Now, to make the emulation perfect, there's only one last thing to settle: I should find a way to unlock the dialog surface when in window mode: hovering the mouse cursor on the menus, you can easily see that only the active menu item shows the textured cursor (the iron glove ...) while the others do not. You can see the difference of behaviour of two apparently similar situations:
      a) "Run in window" unchecked (Diablo in fullscreen mode, the mouse works)
      b) "Run in window" checked, "Position" set to Desktop (mouse troubles ...).

      Any other idea?

      Thank you very much.

       
      Last edit: gho 2014-07-17
  • gho

    gho - 2014-07-17

    Changes incorporated into fresh new v2.02.86 release, together with bilinear filtering.
    If you have a decently powerful computer (not like mine....) you could enjoy diablo in fake fullscreen mode (Desktop mode) and bilinear filtering on. It's not Diablo II, but .... ok, ok, it's just too similar to running it in real fullscreen mode.

    to gsky916

    I had to translate "bilinear filtering" to chinese using google! Please, feel free to propose a surely better translation for next release.

     
    Last edit: gho 2014-07-17
  • aqrit

    aqrit - 2014-07-20

    the extGetDesktopWindow hookproc seems to cause the menu cursor problem?
    Changing it to be just a pass-thru seems to solve the problem.

    HWND WINAPI extGetDesktopWindow(void){
        return (*pGetDesktopWindow)();
    }
    
     
    • gho

      gho - 2014-07-20

      Unbelievable, but true!
      Thank you for this further hint. I think I could have never thought to this specific item in a million years. I'll try to track why that happens, because in general this hook is supposed to be useful (it redirect the desktop to the main window) for instance to avoid clearing the whole desktop or getting information about the real desktop size. So, maybe I could code it more properly... or, in the worst possible case, add another dxwnd configuration flag!
      I'm also working to keep the Diablo control parent window sticked to the main window: this seems to eliminate the game crashes when you move / resize the window in the initial screens.
      Diablo is about to get closer and closer to perfect emulation!

       
  • gho

    gho - 2014-07-20

    Today's update:
    definitely, Diablo is a game full of surpries.
    aqrit, your fix eliminates the invisible mouse problem, but the whole window area becomes undraggable and not resizeable.
    This is not a big problem with current DxWnd releases, since moving the Diablo window results in an almost certain crash. But I'm trying to fix this problem moving together main window and control parent window, and the trick is promising. So, making the window fixed is a pity....
    The window coordination is simple: I store the control parent window somewhere, and within the message loop upon reception of WM_WINDOWPOSCHANGED I move the control parent window at the same coordinates (plus the border, of course).
    The problem with GetDesktopWindow is likely caused by the fact that the desktop window is used to be passes at the GetDC / ReleaseDC API, but again why the lock happens in windowized mode only?
    The good news is the fact that in any case, as soon as the game leaves the user32/gdi32 nightmare and enters into the standard directdraw realm, everything works again perfectly, no invisible mouse and no locked window no more!

     
  • aqrit

    aqrit - 2014-07-21

    Is the game using DeviceContexts for anything other than pallettes and fonts?

    the problem is the storm.dll function that is doing this...

    call    ds:GetWindowThreadProcessId
    call    ds:GetCurrentProcessId
    cmp     [esp+40h+dwProcessId], eax // do we own this window?
    jnz     short loc_15008D87
    

    though at the moment I'm not quite sure why


    Here are some other things that caught my eye:

    if parent is desktop window then don't set "SDlg_Modal" prop and disable

    call    ds:GetDesktopWindow
    cmp     ebx, eax        ; cmp  hWndParent, hWndDesktop
    jz      short loc_150081FB ; jmp if equal
    push    1               ; hData
    push    offset aSdlg_modal ; "SDlg_Modal"
    push    esi             ; hWnd
    call    ds:SetPropA
    

    and check window sytles to determine which code path to take...

    push    0FFFFFFF0h      ; nIndex
    push    eax             ; hWnd
    mov     edi, eax
    call    ebx ; GetWindowLongA
    test    eax, 1000000h   ; WS_MAXIMIZE
    jnz     short loc_1500A046
    push    0FFFFFFECh      ; nIndex
    push    edi             ; hWnd
    call    ebx ; GetWindowLongA
    test    al, 8           ; WS_EX_TOPMOST
    jnz     short loc_1500A019
    
     
  • aqrit

    aqrit - 2014-07-22

    "the problem function" enumerates all windows and adds the cursor rect to the update region of any window which belongs to the current process...

    the "DIABLO" window and the SDlgDialog/WS_EX_CONTROLPARENT are siblings!
    aka. they are both children of the desktop :p

    so when the DIABLO window is passed as the desktop it prevents the game from finding any other windows...

    proof:

    HWND WINAPI extGetDesktopWindow(void)
    {
        BOOL FoundDiabloAtTopLevel = FALSE;
        BOOL FoundControlAtTopLevel = FALSE;
    
        HWND hDesktop = (*pGetDesktopWindow)();
        HWND hDiablo = FindWindow( "DIABLO", NULL );
        HWND hControl = FindWindow( "SDlgDialog", NULL );
        if( ( hDiablo != NULL ) && ( hControl != NULL ) ){
            DWORD dwStyleEx = GetWindowLong( hControl, GWL_EXSTYLE );
            if( ( WS_EX_CONTROLPARENT & dwStyleEx ) && ( dxw.GethWnd() == hDiablo ) ) { // make sure we've these specific windows
                HWND hTemp = GetWindow( hDesktop, GW_CHILD );
                while( hTemp != NULL ){
                    if( hTemp == hDiablo ) FoundDiabloAtTopLevel = TRUE;
                    if( hTemp == hControl ) FoundControlAtTopLevel = TRUE;
                    hTemp = GetWindow( hTemp, GW_HWNDNEXT );
                }
                if( FoundDiabloAtTopLevel == FALSE ) __asm int 3
                if( FoundControlAtTopLevel == FALSE ) __asm int 3
                // if( GetTopWindow( hDiablo ) == NULL ) __asm int 3 // ALWAYS  -- DIABLO has no children!
            }
        }
        return hDesktop;
    }
    
     
    Last edit: aqrit 2014-07-22
    • gho

      gho - 2014-07-22

      Very interesting.
      I suppose the "DIABLO" window is the one that triggers the SetWindowsHookEx callback, and this is why there's no trace of this window creation in the logs.
      It would be interesting to create a general handling of this situation: for instance, it could be possible to create a fake desktop window and let it be the father of all game windows. Unfortunately, this is not possible using the SetWindowsHookEx standard hooking schema, because when DxWnd is triggered, it would be too late: the DIABLO window is created already, son of the real desktop (or is it possible to create a window later and let it be the new father?).
      Maybe it could be possible to do that with DLL injection, where DxWnd starts hooking at the very beginning of the task execution.
      Or maybe there's a simpler solution. I have to think a little about it....

      Update

      Yes, you can create a new fake desktop window and let it become the new father .... but things aren't working better this way. Maybe I made some mistake....

       
      Last edit: gho 2014-07-22
  • aqrit

    aqrit - 2014-07-22

    WinSpy shows the WS_EX_CONTROLPARENT window as having the DIABLO window as both the owner and parent.

    which is why it is so strange that the DIABLO window shows as having no children
    and the WS_EX_CONTROLPARENT window shows as a "top-level" window
    ...

     
    • gho

      gho - 2014-07-22

      WinSpy could be not reliable enough: pointing at the "DIABLO" window, MS Spy++ correctly highlights that window, while WinSpy makes the whole desktop flashing....

       
  • gho

    gho - 2014-07-24

    aqrit, a doubt ....

    I saw that sometimes the window that triggers the SetWindowsHook callback is not child of NULL or the desktop, but it can be a children of a upper level window of the game. For this reason, I was forced to introduce the "Fix parent window" flag that performs all operation NOT on the current hwnd passed to the callback, but to the parent instead.
    This fact has always puzzled me, because (using a static BOOL) I hook the very first window in order of time and it surprises me that there could be a father window belonging to the task but that didn't raise the callback.
    Now, looking in mode detail Diablo's behavior, I got two possible explanation for this oddity:

    1) the SetWindowsHook triggers the callback before processing the window messages. It could be that the father window spawn a child BEFORE any message is sent to its attention, so that the child overtakes the father in the timing sequence?

    2) the father window in NOT a normal window but a message only window (see HWND_MESSAGE on msdn) and this oddity implies that it does not triggers the callback?

    I think that a few log lines should clarify this interesting doubt.
    The "disturbing" thing is the fact that when setting the "Use DLL injection"flag (that is, behaving like a debugger) this situation is altered and I could hook the wrong window.

     
  • aqrit

    aqrit - 2014-07-25

    a few log lines should clarify

    I haven't look into this but:
    If the parent creates a child while processing the WM_NCCREATE message... then problem.
    ( maybe switching over to a WH_CBT hook would help with this. )
    Or could it be a race against SetParent().
    HookProc() doesn't seem thread safe, it should check DoOnce again after acquiring the lock.
    ( because several threads could already be waiting for the lock before the first thread sets the DoOnce variable )

    ( think virtual desktop )
    just because a window is created "first" does not necessarily make it an all encapsulating main window. :-)

    IMO, HookInit shouldn't be doing anything with hwnd's.
    And the "Do we want to hook this process?" and HookInit() could be moved out of HookProc to DllMain's DLL_PROCESS_ATTACH handler. So HookProc might not need to do anything either.

     
    Last edit: aqrit 2014-07-25
    • gho

      gho - 2014-07-25

      About the missing log: here's the output

      HookInit: hWnd class="IME" text="Default IME" style=8c000000(WS_CLIPSIBLINGS+DISABLED+POPUP) exstyle=0(WS_EX_RIGHTSCROLLBAR)
      HookInit: dxw.hParentWnd class="DIABLO" text="DIABLO" style=84000000(WS_CLIPSIBLINGS+POPUP) exstyle=0(WS_EX_RIGHTSCROLLBAR)

      So, it seems that we've got to handle HWND_MESSAGE windows, the "DIABLO" main window, the control parent, the IME, .... a pretty crowded situation, I'd dare say ...

       
  • - 2017-02-01

    I'm happy to say Diablo works near perfect now!
    The only problem is the in-game cursor (hand) not being able to move, and being overlapped by the Windows cursors. It's quite distracting and ugly...

     

Log in to post a comment.