narzoul / DDrawCompat

DirectDraw and Direct3D 1-7 compatibility, performance and visual enhancements for Windows Vista, 7, 8, 10 and 11
BSD Zero Clause License
885 stars 67 forks source link

A little help on "Legal Crime" #219

Closed ghotik closed 7 months ago

ghotik commented 1 year ago

Hi. This old, obscure game (available as a handy packet RIP on oldgames.ru) works perfectly with DDrawCompat (and also with CnC-ddraw, by the way) but it is rendered with an awful color palette with DxWnd. I was wondering if you could have a look and suggest me what DxWnd is doing wrong? I believe the problem is in GDI handling, the executable links the GDI32 CreatePalette and GetNearestPaletteIndex, is this a possible clue? P.s. it is mostly for my curiosity, because if you want to run the game in a windw the combination of DDrawCompat + DxWnd works.

elishacloud commented 1 year ago

I don't think it has anything to do with GDI handling of palettes because the colors work fine with dxwrapper and I don't hook any GDI palettes.

What might be happening is that the palette is changing in the IDirectDrawPalette::SetEntries() function and DxWnd is not drawing the screen with the new palettes after that. I had that issue with dxwrapper and added code here to handle that case.

But keep in mind that both dxwrapper and CnC-ddraw convert the ddraw functions to d3d9, so we are not reliant to how ddraw/GDI handles things.

ghotik commented 1 year ago

Thank you for the suggestion. Excluding the GDI32 as a possible culprit will help to shrink the research area. DxWnd handles the SetEntries palette updates, but it is possible that the handling is failing on some peculiar situation (maybe about reserved entries handling, or 256 bounds overflow, who knows ..). I'll revise the logs and my code in search of a possible flaw.

ghotik commented 1 year ago

Looking at the game logs and a picture comparison, it seems there's some different problem behind all this. First of all, there are no traces of IDirectDrawPalette::SetEntries() in the logs. Instead, the game makes a strange alternate of Ddraw palette and GDI palette operations, I think that in some case it calls GetDC to get a GDI handle of the surface and applies a GDI palette over it. But in any case the color switch happens, though looking at certain areas of the screenshots (fortunately the game also runs natively on Win11 by setting the 8bit reduced mode compatibility option) it seems rather that it tries to get some color by using a 4 bit (16 colors) mode and applying dithering. In the logs there are also traces of GDI GetNearestPaletteIndex(), that is a quite rarely used system call. All this makes it a quite interesting case. comparison

ghotik commented 1 year ago

Hi! I'm bumping on this thread again. I got some more understanding about what's going on (see here https://sourceforge.net/p/dxwnd/discussion/general/thread/385af88904/?page=1#70b6 if you want). The scrambled screens are built transferring a bitmap to the backbuffer surface DC by getting the backbuffer DC with GetDC followed by a StretchDIBits call. My dumps show that the BMI bitmap is correct (8bit palettized with all the 256 color shades) while the transferred image is heavily dithered as if the palette was not available. This doesnt happen if the backbuffer is built with the desktop native 32bit color mode, in that case GDI seems able to handle the situation, but that way I got other troubles in other game screens, so this doesn't seem a solution. Have you any idea about why this happens?

narzoul commented 1 year ago

GetDC uses D3DKMTCreateDCFromMemory (at least on WDDM, not sure about XPDM) to create the DC. It receives a pColorData parameter, which is a pointer to the palette (array of PALETTEENTRY structs) that the DC will use. The default color table that the runtime passes in is probably incorrect, because it has no idea what the system/hardware palette should be when 8-bit color more is only emulated. Check the hook for this function in DDrawCompat, which might give you some ideas how to fix it: https://github.com/narzoul/DDrawCompat/blob/7a59458d585fbf7eb1b243fc133c091bbdf561ce/DDrawCompat/D3dDdi/KernelModeThunks.cpp#L54

elishacloud commented 1 year ago

@ghotik, I had a similar issue with dxwrapper. The issue is that the default stretch mode is linear. This means when it is stretching the DC is adds colors in between two of the palette colors. I had to set the stretch mode to HALFTONE to solve this issue.

See comments here: https://www.herdsoft.com/ti/davincie/leon8vlg.htm

StretchDIBits uses the stretch mode of the target device context as set by SetStretchBltMode to determine how to stretch or scale down the bitmap. When rescaling DIBs of biBitCount <=8 to smaller sizes, the default often produces ugly stripes on the target image.

See dxwrapper code here: https://github.com/elishacloud/dxwrapper/blob/aa2468272c7de861d57bed2b7d3f9aa85292d05a/ddraw/IDirectDrawSurfaceX.cpp#L4816

See here: https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setstretchbltmode

ghotik commented 1 year ago

Thank you so much, I'm almost arrived to a final result, but still have some curious problem. First of all, I tried both approaches, but the HALFTONE solution didn't work, then I tried the Narzoul's suggestion. Translated to dxwnd simpler code, the idea was roughly implemented here:

long WINAPI DxCreateDCFromMemory(D3DKMT_CREATEDCFROMMEMORY *pData)
{
    DWORD ret;
    ApiName("DxCreateDCFromMemory");
    extern long D3DKMTCreateDCFromMemory(D3DKMT_CREATEDCFROMMEMORY *);
    PALETTEENTRY *origColorTable = pData->pColorTable;
    DWORD origFormat = pData->Format;
    if(pCreateDCFromMemory == 0){
        HINSTANCE hinst=(*pLoadLibraryA)("gdi32.dll");
        if(!hinst){
            OutTrace("%s: LoadLibrary ddraw.dll ERROR err=%d @%d\n", ApiRef, GetLastError(), __LINE__);
            return 0;
        }
        pCreateDCFromMemory = (CreateDCFromMemory_Type)(*pGetProcAddress)(hinst, "D3DKMTCreateDCFromMemory");
        (*pFreeLibrary)(hinst);
    }
    if(!pCreateDCFromMemory) return 0;
    if(D3DDDIFMT_P8 == pData->Format){
        OutTrace("D3DKMTCreateDCFromMemory 8bit DC\n");
        extern DWORD PaletteEntries[256];
        for(int i=0; i<256; i++){
            palette[i].peRed = (PaletteEntries[i] & 0x0000FF);
            palette[i].peGreen = (PaletteEntries[i] & 0xFF00) >> 8;
            palette[i].peBlue = (PaletteEntries[i] & 0xFF0000) >> 16;
            palette[i].peFlags = 0;
        }
        pData->pColorTable = palette;
    }
    ret = (*pCreateDCFromMemory)(pData);
    pData->pColorTable = origColorTable;
    pData->Format = origFormat;
    return ret;
}

where DxCreateDCFromMemory is my hooked replacement for D3DKMTCreateDCFromMemory and PaletteEntries is a DWORD array with the emulated palette in RGB format. The code works as a charm, but there is a caveat: the red color seems gone! The culprit can't be my palette because this happen also when I force the red to full intensity replacing one line of code: palette[i].peRed = 0xFF; But, before bumping my head and trying every possible meaningless bit mask & shift combination, I noted that in Narzoul's code there is a sort of "special treatment" for the red color: if (palette[i].peFlags & PC_EXPLICIT) { palette[i] = sysPal[palette[i].peRed]; } Can you explain why with my simple palette transfer I get this sort of result (note: it's almost perfect, I should just get some red...) : almost almost2

ghotik commented 1 year ago

Update: Forgive my silly mistake about a byte overflow: palette[i].peRed = (byte)((PaletteEntries[i] & 0xFF0000) >> 16); This will teach me to never ignore a compiler's warinig. Now it's so good!!! perfect perfect2 Thank you again!

elishacloud commented 1 year ago

I'm just curious, but if the issue is that the DC created with GetDC() is created with the wrong palette then wouldn't it be easier to just call SetDIBColorTable() on the DC after the application calls GetDC() but before the DC handle is returned to the application?

narzoul commented 1 year ago

I think I tried that a long time ago and it didn't work, but I may be misremembering.

Another idea is using SetPalette on the surface before calling GetDC. It's possible that the palette is only incorrect when there is no explicit palette attached to the surface.

ghotik commented 1 year ago

I hope to be able to satisfy your curiosity making more tests on alternative solutions: I'm curious too, because this DC palette problem happens in many other different situations. For instance, "Loony Labyrinth Pinball" has problems when trying to color the intro panels in a pure GDI situation, where hacking ddraw.dll would be no use. The problem is that the DC is a compatible DC with the desktop, with 32bit colors and where the palette methods have no effect. When I operate on emulated ddraw surfaces I have 8bit surfaces that work with palette, but the game intro happens before the ddraw session creation. More than this, the DIB are faded using a looped AnimatePalette sequence that can be hooked, but it has no reference to the DIB and not even to the DC! But I think I'd better open another thread on this subject.