From b71e8f795d8c2c6ae8f78148a639a6ee94ec4cc3 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 27 Apr 2026 06:15:10 +0200 Subject: [PATCH 1/5] DX8 and DX9 GUIDs cannot live within the same translation unit, have to split them up into separate files --- Makefile.common | 10 ++++++++++ gfx/common/dx_guids.c | 22 +++++++++++++++------- gfx/common/dx_guids_d3d8.c | 26 ++++++++++++++++++++++++++ gfx/common/dx_guids_d3d9.c | 26 ++++++++++++++++++++++++++ gfx/drivers/d3d8.c | 2 +- 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 gfx/common/dx_guids_d3d8.c create mode 100644 gfx/common/dx_guids_d3d9.c diff --git a/Makefile.common b/Makefile.common index a0ad16c9366b..97af05f52dae 100644 --- a/Makefile.common +++ b/Makefile.common @@ -2549,6 +2549,16 @@ ifneq ($(findstring Win32,$(OS)),) gfx/display_servers/dispserv_win32.o \ gfx/common/dx_guids.o + # Legacy pre-DXGI GUID storage in their own TUs to avoid clashes + # between system / and the bundled + # . See dx_guids.c header comment. + ifeq ($(HAVE_D3D9), 1) + OBJ += gfx/common/dx_guids_d3d9.o + endif + ifeq ($(HAVE_D3D8), 1) + OBJ += gfx/common/dx_guids_d3d8.o + endif + ifeq ($(HAVE_GDI), 1) OBJ += gfx/drivers/gdi_gfx.o LIBS += -lmsimg32 diff --git a/gfx/common/dx_guids.c b/gfx/common/dx_guids.c index bab2d06b9e81..483bc7f144a5 100644 --- a/gfx/common/dx_guids.c +++ b/gfx/common/dx_guids.c @@ -5,13 +5,25 @@ * The Windows 7.x Platform SDK (bundled with MSVC 2010 and earlier) * does not ship dxguid.lib. Rather than require the legacy June 2010 * DirectX SDK just to pick up a handful of IID_* / CLSID_* constants, - * we define them ourselves in this single translation unit. + * we define them ourselves. * * Including before any DX header causes DEFINE_GUID() to * expand to actual storage rather than extern references. Every other * TU in the program continues to see the default extern declarations * and resolves the GUIDs to the storage defined here at link time. * + * GUID storage for the modern (DXGI-based) DX stack lives in this file + * (D3D10/11/12, DXGI, D3DCompiler, DInput, XAudio). The legacy pre-DXGI + * stack lives in two separate TUs: + * * gfx/common/dx_guids_d3d9.c + * * gfx/common/dx_guids_d3d8.c + * because / and / cannot share a + * single TU portably -- system / headers + * (notably mingw-w64) do not honor the D3DCOLORVALUE_DEFINED guard + * used by the bundled gfx/include/dxsdk headers, and redefine + * D3DCOLORVALUE / D3DVECTOR / D3DMATRIX. Splitting per header family + * sidesteps that altogether. + * * IMPORTANT: no other TU in the program may include * before DirectX headers, or the linker will report duplicate symbols * for IID_* / CLSID_* constants. @@ -42,12 +54,8 @@ #ifdef HAVE_D3D10 #include #endif -#ifdef HAVE_D3D9 -#include -#endif -#ifdef HAVE_D3D8 -#include -#endif +/* HAVE_D3D9: emitted in gfx/common/dx_guids_d3d9.c */ +/* HAVE_D3D8: emitted in gfx/common/dx_guids_d3d8.c */ #if defined(HAVE_D3D10) || defined(HAVE_D3D11) || defined(HAVE_D3D12) \ || (defined(HAVE_D3D9) && defined(HAVE_HLSL)) diff --git a/gfx/common/dx_guids_d3d8.c b/gfx/common/dx_guids_d3d8.c new file mode 100644 index 000000000000..36742f5d6f2d --- /dev/null +++ b/gfx/common/dx_guids_d3d8.c @@ -0,0 +1,26 @@ +/* dx_guids_d3d8.c + * + * Emits Direct3D 8 COM GUID storage in lieu of linking dxguid.lib. + * + * Split out from dx_guids.c because the legacy header family + * (pre-DXGI) and the modern / header family cannot + * coexist in a single translation unit on every toolchain. The bundled + * gfx/include/dxsdk headers and the bundled gfx/include/d3d8 headers + * cooperate via shared D3DCOLORVALUE_DEFINED / D3DVECTOR_DEFINED / + * D3DMATRIX_DEFINED guards, but a system (e.g. mingw-w64) + * will not honor those guards, redefining D3DCOLORVALUE et al. and + * breaking the build. + * + * Including before causes DEFINE_GUID() in + * gfx/include/d3d8/d3d8.h (or in the system d3d8.h) to expand to actual + * storage. See dx_guids.c for the rest of the design rationale. + */ + +#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_GRIFFIN) \ + && defined(HAVE_D3D8) \ + && (!defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP)) + +#include +#include + +#endif /* _WIN32 && !_XBOX && !HAVE_GRIFFIN && HAVE_D3D8 && desktop */ diff --git a/gfx/common/dx_guids_d3d9.c b/gfx/common/dx_guids_d3d9.c new file mode 100644 index 000000000000..4c751405d77c --- /dev/null +++ b/gfx/common/dx_guids_d3d9.c @@ -0,0 +1,26 @@ +/* dx_guids_d3d9.c + * + * Emits Direct3D 9 COM GUID storage in lieu of linking dxguid.lib. + * + * Split out from dx_guids.c because the legacy header family + * (pre-DXGI) and the modern / header family cannot + * coexist in a single translation unit on every toolchain. The bundled + * gfx/include/dxsdk headers and the bundled gfx/include/d3d9 headers + * cooperate via shared D3DCOLORVALUE_DEFINED / D3DVECTOR_DEFINED / + * D3DMATRIX_DEFINED guards, but a system (e.g. mingw-w64) + * will not honor those guards, redefining D3DCOLORVALUE et al. and + * breaking the build. + * + * Including before causes DEFINE_GUID() in + * gfx/include/d3d9/d3d9.h (or in the system d3d9.h) to expand to actual + * storage. See dx_guids.c for the rest of the design rationale. + */ + +#if defined(_WIN32) && !defined(_XBOX) && !defined(HAVE_GRIFFIN) \ + && defined(HAVE_D3D9) \ + && (!defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP)) + +#include +#include + +#endif /* _WIN32 && !_XBOX && !HAVE_GRIFFIN && HAVE_D3D9 && desktop */ diff --git a/gfx/drivers/d3d8.c b/gfx/drivers/d3d8.c index 4424618e7aee..af03aeb2d291 100644 --- a/gfx/drivers/d3d8.c +++ b/gfx/drivers/d3d8.c @@ -825,7 +825,7 @@ static void d3d8_overlay_render(d3d8_video_t *d3d, struct video_viewport vp; unsigned i; Vertex vert[4]; - enum D3DTEXTUREFILTERTYPE filter_type = D3DTEXF_LINEAR; + D3DTEXTUREFILTERTYPE filter_type = D3DTEXF_LINEAR; if (!d3d || !overlay || !overlay->tex) return; From 4d16b03f9e9f90a2659b86c6b545cffa3272c550 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 27 Apr 2026 06:38:53 +0200 Subject: [PATCH 2/5] gfx/d3d8, gfx/d3d9: surface dylib_load failure for d3d{8,9}.dll Both drivers' *_initialize_symbols() return false silently when LoadLibrary("d3d8.dll") / ("d3d9.dll") fails. The caller in turn returns NULL silently from the video driver's init(), so the only diagnostic the user gets is the generic [ERROR] [Video] Cannot open video driver. Exiting... from video_driver_init_internal(), with no hint that the legacy runtime is the problem. Even --verbose does not help, because there is no log call between the driver being selected and init() returning NULL. This is increasingly common on modern Windows installs that ship only d3d8thk.dll (the kernel thunk layer) and not the user-mode d3d8.dll runtime; recently made more visible by the qb change that lets D3D8 build without d3d8.lib being available at link time, which means more people end up running a d3d8-enabled binary on a system where the runtime DLL is missing. Falling back to d3d8thk.dll is not an option -- it does not export Direct3DCreate8, it is only the GDI-side thunk syscalls. So just log clearly and let the caller fail. Log dylib_error() (already populated by dylib_load on Win32 via set_dl_err) and a one-liner pointing the user at the legacy DX runtime or at picking a different video driver. Also log if the DLL loads but Direct3DCreate{8,9} cannot be resolved, since that path is silent too. No behavioural change beyond the new log lines; the error return is unchanged. --- gfx/common/d3d9_common.c | 13 ++++++++++++- gfx/drivers/d3d8.c | 19 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/gfx/common/d3d9_common.c b/gfx/common/d3d9_common.c index 3e6e5a6f6568..4a338418c515 100644 --- a/gfx/common/d3d9_common.c +++ b/gfx/common/d3d9_common.c @@ -72,8 +72,19 @@ bool d3d9_initialize_symbols(enum gfx_ctx_api api) if (!(g_d3d9_dll = dylib_load("d3d9d.dll"))) #endif if (!(g_d3d9_dll = dylib_load("d3d9.dll"))) + { + /* On a system where the D3D9 user-mode runtime is missing, the + * caller would otherwise see only the generic "Cannot open video + * driver" message. Surface the real cause here. */ + RARCH_ERR("[D3D9] Failed to load d3d9.dll: %s\n", + dylib_error() ? dylib_error() : "(no error reported)"); + RARCH_ERR("[D3D9] The DirectX 9 runtime is not present on this " + "system. Install it or pick a different video driver.\n"); return false; - D3D9Create = (D3D9Create_t)dylib_proc(g_d3d9_dll, "Direct3DCreate9"); + } + if (!(D3D9Create = (D3D9Create_t)dylib_proc(g_d3d9_dll, "Direct3DCreate9"))) + RARCH_ERR("[D3D9] d3d9.dll does not export Direct3DCreate9: %s\n", + dylib_error() ? dylib_error() : "(no error reported)"); #else D3D9Create = Direct3DCreate9; #endif diff --git a/gfx/drivers/d3d8.c b/gfx/drivers/d3d8.c index af03aeb2d291..d3032d727d3e 100644 --- a/gfx/drivers/d3d8.c +++ b/gfx/drivers/d3d8.c @@ -215,8 +215,25 @@ static bool d3d8_initialize_symbols(enum gfx_ctx_api api) g_d3d8_dll = dylib_load("d3d8.dll"); if (!g_d3d8_dll) + { + /* On modern Windows the legacy D3D8 user-mode runtime is not + * installed by default (only d3d8thk.dll, the kernel thunk + * layer, ships with the OS). Tell the user explicitly -- + * otherwise the only message they see is the generic + * "Cannot open video driver" from video_driver_init_internal. */ + RARCH_ERR("[D3D8] Failed to load d3d8.dll: %s\n", + dylib_error() ? dylib_error() : "(no error reported)"); + RARCH_ERR("[D3D8] The legacy DirectX 8 runtime is not present " + "on this system. Install it (e.g. via the legacy DirectX " + "End-User Runtimes from Microsoft) or pick a different " + "video driver.\n"); return false; - D3DCreate = (D3DCreate_t)dylib_proc(g_d3d8_dll, "Direct3DCreate8"); + } + if (!(D3DCreate = (D3DCreate_t)dylib_proc(g_d3d8_dll, "Direct3DCreate8"))) + { + RARCH_ERR("[D3D8] d3d8.dll does not export Direct3DCreate8: %s\n", + dylib_error() ? dylib_error() : "(no error reported)"); + } #else D3DCreate = Direct3DCreate8; #endif From 67e2a9e363db08bdd4a861b1952fb864819ce729 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 27 Apr 2026 07:09:49 +0200 Subject: [PATCH 3/5] (D3D8) Update warning message --- gfx/drivers/d3d8.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/gfx/drivers/d3d8.c b/gfx/drivers/d3d8.c index d3032d727d3e..72b8bb4d2b98 100644 --- a/gfx/drivers/d3d8.c +++ b/gfx/drivers/d3d8.c @@ -224,8 +224,8 @@ static bool d3d8_initialize_symbols(enum gfx_ctx_api api) RARCH_ERR("[D3D8] Failed to load d3d8.dll: %s\n", dylib_error() ? dylib_error() : "(no error reported)"); RARCH_ERR("[D3D8] The legacy DirectX 8 runtime is not present " - "on this system. Install it (e.g. via the legacy DirectX " - "End-User Runtimes from Microsoft) or pick a different " + "on this system. Drop a matching d3d8.dll and d3d9.dll" + "(e.g. from DXVK) next to retroarch.exe, or pick a different " "video driver.\n"); return false; } @@ -1404,17 +1404,20 @@ static void d3d8_set_nonblock_state(void *data, bool state, bool adaptive_vsync_enabled, unsigned swap_interval) { - int interval = 0; - d3d8_video_t *d3d = (d3d8_video_t*)data; +#ifdef _XBOX + int interval = 0; +#endif + d3d8_video_t *d3d = (d3d8_video_t*)data; if (!d3d) return; - if (!state) - interval = 1; d3d->video_info.vsync = !state; #ifdef _XBOX + if (!state) + interval = 1; + IDirect3DDevice8_SetRenderState(d3d->dev, D3D8_PRESENTATIONINTERVAL, interval ? From 71aede9632f319a08034bfa52e94623945a0c7fd Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 27 Apr 2026 07:23:28 +0200 Subject: [PATCH 4/5] gfx/d3d8: Upload RGUI menu framebuffer directly as ARGB4444 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The D3D8 driver previously expanded RGUI's 16bpp menu framebuffer to 32bpp on the CPU every frame via a per-pixel loop, then uploaded the result into a D3DFMT_A8R8G8B8 texture. RGUI already assembles its framebuffer in 16bpp; D3D8 has supported D3DFMT_A4R4G4B4 as a baseline texture format since launch, on both PC and Original Xbox. Add D3D8_ARGB4444_FORMAT to the existing D3D8 format macros (with D3DFMT_LIN_A4R4G4B4 for the _XBOX build path), and a "d3d8" case to the RGUI pixel format dispatcher selecting argb32_to_argb4444 (already in use by the rsx/PS3 and d3d9_hlsl/d3d9_cg drivers, which target the same ARGB4444 bit layout). In d3d8_set_menu_texture_frame, allocate the menu texture as D3DFMT_A4R4G4B4 when rgb32 is false (the only case in current practice; RGUI is the sole caller), and upload row-by-row via memcpy. The rgb32 = true API branch is preserved for forward compatibility and continues to use D3DFMT_A8R8G8B8. Track the bpp of the currently-allocated menu texture in a new d3d8_video_t::menu_tex_rgb32 field so the texture is recreated when the rgb32 flag flips between calls — same approach the D3D9 patch uses for d3d9_video_t. Endian-safe by construction: argb32_to_argb4444 produces a host-endian uint16_t with A in bits 15..12 down to B in 3..0; D3DFMT_A4R4G4B4 is read by D3D as host-endian 16-bit units with the same bit assignments. D3D8 only targets LE hosts (Pentium III on Original Xbox, x86/x64 on legacy Windows), so there is no byte-swap concern in either direction. Xbox-specific tex_coords[2]/[3] assignment after texture (re)creation is preserved. --- gfx/drivers/d3d8.c | 60 +++++++++++++++++++++++++++++---------------- menu/drivers/rgui.c | 1 + 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/gfx/drivers/d3d8.c b/gfx/drivers/d3d8.c index 72b8bb4d2b98..ba55c73b49f4 100644 --- a/gfx/drivers/d3d8.c +++ b/gfx/drivers/d3d8.c @@ -79,10 +79,12 @@ #define D3D8_RGB565_FORMAT D3DFMT_LIN_R5G6B5 #define D3D8_XRGB8888_FORMAT D3DFMT_LIN_X8R8G8B8 #define D3D8_ARGB8888_FORMAT D3DFMT_LIN_A8R8G8B8 +#define D3D8_ARGB4444_FORMAT D3DFMT_LIN_A4R4G4B4 #else #define D3D8_RGB565_FORMAT D3DFMT_R5G6B5 #define D3D8_XRGB8888_FORMAT D3DFMT_X8R8G8B8 #define D3D8_ARGB8888_FORMAT D3DFMT_A8R8G8B8 +#define D3D8_ARGB4444_FORMAT D3DFMT_A4R4G4B4 #endif typedef struct d3d8_video @@ -128,6 +130,14 @@ typedef struct d3d8_video /* Only used for Xbox */ bool widescreen_mode; + + /* Bit-depth of the data most recently uploaded to `menu->tex`. + * The menu texture is created with a fixed pixel format (16bpp + * ARGB4444 for the RGUI fast path, 32bpp ARGB8888 otherwise), + * so we must recreate it when set_menu_texture_frame is called + * with a different `rgb32` value. Defaults to false; the first + * call will see a NULL tex and create one regardless. */ + bool menu_tex_rgb32; } d3d8_video_t; typedef struct d3d8_renderchain @@ -2000,17 +2010,25 @@ static void d3d8_set_menu_texture_frame(void *data, if (!d3d || !d3d->menu) return; - if ( !d3d->menu->tex || - d3d->menu->tex_w != width || - d3d->menu->tex_h != height) + if ( !d3d->menu->tex || + d3d->menu->tex_w != width || + d3d->menu->tex_h != height || + d3d->menu_tex_rgb32 != rgb32) { LPDIRECT3DTEXTURE8 tex = d3d->menu->tex; if (tex) IDirect3DTexture8_Release(tex); + /* RGUI sends 16bpp ARGB4444 (the d3d8 case in RGUI's pixel + * format dispatcher selects argb32_to_argb4444), so we can + * upload it byte-for-byte into a D3DFMT_A4R4G4B4 texture and + * skip the per-pixel CPU expansion to ARGB8888 the previous + * implementation did every frame. The rgb32 path is preserved + * for callers that hand us 32bpp data; in current practice no + * such caller exists, but the API contract supports it. */ d3d->menu->tex = d3d8_texture_new(d3d->dev, width, height, 1, - 0, D3D8_ARGB8888_FORMAT, + 0, rgb32 ? D3D8_ARGB8888_FORMAT : D3D8_ARGB4444_FORMAT, D3DPOOL_MANAGED, 0, 0, 0, NULL, NULL, false); if (!d3d->menu->tex) @@ -2018,6 +2036,7 @@ static void d3d8_set_menu_texture_frame(void *data, d3d->menu->tex_w = width; d3d->menu->tex_h = height; + d3d->menu_tex_rgb32 = rgb32; #ifdef _XBOX d3d->menu->tex_coords [2] = width; d3d->menu->tex_coords[3] = height; @@ -2031,7 +2050,7 @@ static void d3d8_set_menu_texture_frame(void *data, if (IDirect3DTexture8_LockRect(tex, 0, &d3dlr, NULL, D3DLOCK_NOSYSLOCK) == D3D_OK) { - unsigned h, w; + unsigned h; if (rgb32) { @@ -2047,24 +2066,23 @@ static void d3d8_set_menu_texture_frame(void *data, } else { - uint32_t *dst = (uint32_t*)d3dlr.pBits; - const uint16_t *src = (const uint16_t*)frame; + /* Direct ARGB4444 upload. The bit layout produced by + * argb32_to_argb4444 (host-endian uint16_t with A in bits + * 15..12, R 11..8, G 7..4, B 3..0) matches D3DFMT_A4R4G4B4 + * exactly: D3D reads the locked memory as host-endian + * 16-bit units with the same bit assignments, so the same + * source bytes work on LE PC and LE Original Xbox (NV2A + * via D3DFMT_LIN_*) without a byte swap. */ + uint8_t *dst = (uint8_t*)d3dlr.pBits; + const uint8_t *src = (const uint8_t*)frame; + unsigned src_pitch = width * sizeof(uint16_t); + unsigned row_bytes = width * sizeof(uint16_t); - for (h = 0; h < height; h++, dst += d3dlr.Pitch >> 2, src += width) + for (h = 0; h < height; h++, dst += d3dlr.Pitch, src += src_pitch) { - for (w = 0; w < width; w++) - { - uint16_t c = src[w]; - uint32_t r = (c >> 12) & 0xf; - uint32_t g = (c >> 8) & 0xf; - uint32_t b = (c >> 4) & 0xf; - uint32_t a = (c >> 0) & 0xf; - r = ((r << 4) | r) << 16; - g = ((g << 4) | g) << 8; - b = ((b << 4) | b) << 0; - a = ((a << 4) | a) << 24; - dst[w] = r | g | b | a; - } + memcpy(dst, src, row_bytes); + if (d3dlr.Pitch > (int)row_bytes) + memset(dst + row_bytes, 0, d3dlr.Pitch - row_bytes); } } diff --git a/menu/drivers/rgui.c b/menu/drivers/rgui.c index 0744f416c6d9..be94f6a09498 100644 --- a/menu/drivers/rgui.c +++ b/menu/drivers/rgui.c @@ -1397,6 +1397,7 @@ static bool rgui_set_pixel_format_function(void) else if (string_is_equal(driver_ident, "psp1")) /* PSP */ argb32_to_pixel_platform_format = argb32_to_abgr4444; else if ( string_is_equal(driver_ident, "rsx") /* PS3 */ + || string_is_equal(driver_ident, "d3d8") /* D3D8 (Original Xbox + legacy Windows) */ || string_is_equal(driver_ident, "d3d9_hlsl") /* D3D9 (PC + Xbox 360) */ || string_is_equal(driver_ident, "d3d9_cg")) argb32_to_pixel_platform_format = argb32_to_argb4444; From 5f23e85a3ebc2224e0f2fe968473c2b93ce4a449 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 27 Apr 2026 09:21:14 +0200 Subject: [PATCH 5/5] (D3D8) Widgets and gfx_display fully implemented --- configuration.c | 3 +- gfx/drivers/d3d8.c | 1006 ++++++++++++++++++++++++++++++++++++++++++-- gfx/font_driver.c | 1 + gfx/font_driver.h | 1 + 4 files changed, 983 insertions(+), 28 deletions(-) diff --git a/configuration.c b/configuration.c index dd9082bf9343..a2243c9a7a14 100644 --- a/configuration.c +++ b/configuration.c @@ -3576,7 +3576,8 @@ static bool check_menu_driver_compatibility(settings_t *settings) switch (video_driver[0]) { case 'd': - return (memcmp(video_driver, "d3d9_hlsl", 9) == 0 && video_driver[9] == '\0') + return (memcmp(video_driver, "d3d8", 4) == 0 && video_driver[4] == '\0') + || (memcmp(video_driver, "d3d9_hlsl", 9) == 0 && video_driver[9] == '\0') || (memcmp(video_driver, "d3d9_cg", 7) == 0 && video_driver[7] == '\0') || (memcmp(video_driver, "d3d10", 5) == 0 && video_driver[5] == '\0') || (memcmp(video_driver, "d3d11", 5) == 0 && video_driver[5] == '\0') diff --git a/gfx/drivers/d3d8.c b/gfx/drivers/d3d8.c index ba55c73b49f4..17907facc718 100644 --- a/gfx/drivers/d3d8.c +++ b/gfx/drivers/d3d8.c @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -66,6 +67,7 @@ #endif #include "../font_driver.h" +#include "../gfx_display.h" #include "../../core.h" #include "../../retroarch.h" @@ -110,6 +112,26 @@ typedef struct d3d8_video void *decl; int size; int offset; + /* Soft scissor for D3D8 (no SetScissorRect available). + * scissor_begin stores the requested rect here, and + * gfx_display_d3d8_draw skips any quad whose screen-space + * bounding box lies entirely outside the rect. Partial + * overlaps still draw in full — true geometry clipping + * would require modifying vertex/UV arrays per draw and is + * not worth the complexity here. Skip-only is enough to + * stop entry lists from spilling on top of header/footer + * regions in Ozone, which is the only place the visual + * gap with d3d9+ was noticeable. */ + int scissor_x; + int scissor_y; + int scissor_w; + int scissor_h; + bool scissor_active; + /* Scratch UV array for clipped quads. Layout matches + * d3d8_tex_coords: BL, BR, TL, TR (8 floats). Reused + * across draws — only valid until the next clipped + * draw. */ + float scissor_uv[8]; }menu_display; overlay_t *overlays; @@ -695,9 +717,156 @@ static void gfx_display_d3d8_draw(gfx_display_ctx_draw_t *draw, const float *vertex = NULL; const float *tex_coord = NULL; const float *color = NULL; + /* When the soft-scissor clipping path remaps UVs, it points + * this at d3d->menu_display.scissor_uv and the per-vertex + * read below uses it instead of draw->coords->tex_coord. */ + const float *clipped_uv = NULL; - if (!d3d || !draw || draw->pipeline_id) + if (!d3d || !draw) return; + if (!draw->coords) + return; + + /* Soft scissor. + * + * D3D8 has no SetScissorRect, so we approximate scissoring in + * software inside the draw function itself. Two strategies + * depending on what the caller supplies: + * + * - Default-vertex path (draw->coords->vertex == NULL, i.e. + * gfx_display_draw_quad): we have an axis-aligned screen + * rect from draw->x/y/width/height, plus a 4-element UV + * array (either the caller's tex_coord or the default + * [0..1] one). We clip the rect against the scissor and + * remap the UVs proportionally so the visible portion of + * the texture still lands on the visible portion of the + * screen rect. This is what Ozone's entry icons, + * selection borders and dividers use, and the fully + * correct path for the cases that overflow. + * + * - Explicit-vertex path (draw->coords->vertex != NULL, i.e. + * gfx_display_draw_texture_slice with its 9 sub-quads): + * the geometry is already in normalised [0,1] screen + * space and clipping each of the 9 sub-quads with UV + * remap is too invasive. Fall back to skip-only — only + * drop sub-quads whose bounding box lies entirely outside + * the scissor rect. + * + * Note that gfx_display_draw_quad converts the caller's + * top-down Y into bottom-up via draw->y = height - y - h. We + * convert back to top-down here for the comparison and back + * again on the way out, so callers don't notice. */ + if (d3d->menu_display.scissor_active) + { + int sx = d3d->menu_display.scissor_x; + int sy = d3d->menu_display.scissor_y; + int sx2 = sx + d3d->menu_display.scissor_w; + int sy2 = sy + d3d->menu_display.scissor_h; + + if (draw->coords->vertex) + { + /* Skip-only path for explicit-vertex draws. Build a + * bounding box from the vertex array (normalised + * [0,1] screen space, Y bottom-up) and skip if it's + * entirely outside the scissor. */ + float vmin_x = draw->coords->vertex[0]; + float vmin_y = draw->coords->vertex[1]; + float vmax_x = vmin_x; + float vmax_y = vmin_y; + int qx, qy, qx2, qy2; + unsigned vi; + for (vi = 1; vi < draw->coords->vertices; vi++) + { + float vx = draw->coords->vertex[vi * 2 + 0]; + float vy = draw->coords->vertex[vi * 2 + 1]; + if (vx < vmin_x) vmin_x = vx; + if (vx > vmax_x) vmax_x = vx; + if (vy < vmin_y) vmin_y = vy; + if (vy > vmax_y) vmax_y = vy; + } + qx = (int)(vmin_x * (float)video_width); + qx2 = (int)(vmax_x * (float)video_width); + qy2 = (int)((1.0f - vmin_y) * (float)video_height); + qy = (int)((1.0f - vmax_y) * (float)video_height); + + if (qx2 <= sx || qx >= sx2 || qy2 <= sy || qy >= sy2) + return; + } + else + { + /* Geometry-clipping path for default-vertex draws. + * Clip the screen rect against the scissor and remap + * the UVs proportionally; we mutate draw->x/y/w/h and + * a local UV copy in place, then fall through to the + * normal rendering code with the clipped values. */ + int qx_left = draw->x; + int qx_right = draw->x + (int)draw->width; + int qy_bot = (int)video_height - draw->y; /* top-down */ + int qy_top = qy_bot - (int)draw->height; /* top-down */ + int new_left = qx_left > sx ? qx_left : sx; + int new_right = qx_right < sx2 ? qx_right : sx2; + int new_top = qy_top > sy ? qy_top : sy; + int new_bot = qy_bot < sy2 ? qy_bot : sy2; + + if (new_left >= new_right || new_top >= new_bot) + return; + + /* Only mutate if the rect actually clips, to keep the + * common (no overlap with scissor edges) path free of + * UV remapping noise and to avoid the float roundtrip. */ + if ( new_left != qx_left || new_right != qx_right + || new_top != qy_top || new_bot != qy_bot) + { + const float *src_uv = draw->coords->tex_coord + ? draw->coords->tex_coord + : &d3d8_tex_coords[0]; + float w_orig = (float)draw->width; + float h_orig = (float)draw->height; + float fx_l = (float)(new_left - qx_left) / w_orig; + float fx_r = (float)(new_right - qx_left) / w_orig; + float fy_t = (float)(new_top - qy_top) / h_orig; + float fy_b = (float)(new_bot - qy_top) / h_orig; + /* Source UVs in BL,BR,TL,TR order match d3d8_tex_coords: + * src_uv[0,1] = BL src_uv[2,3] = BR + * src_uv[4,5] = TL src_uv[6,7] = TR + * The four corners share U-left/U-right and V-top/V-bot, + * so derive those from BL/BR (U) and TL/BL (V). */ + float u_l_orig = src_uv[0]; /* BL.u */ + float u_r_orig = src_uv[2]; /* BR.u */ + float v_t_orig = src_uv[5]; /* TL.v */ + float v_b_orig = src_uv[1]; /* BL.v */ + float u_l_new = u_l_orig + fx_l * (u_r_orig - u_l_orig); + float u_r_new = u_l_orig + fx_r * (u_r_orig - u_l_orig); + /* V interpolates from v_t_orig (top, fy=0) to v_b_orig + * (bot, fy=1), i.e. fy_t/fy_b are along the top->bot + * axis. The default tex coord array has TL.v=0 and + * BL.v=1 so V grows downward, matching D3D convention. */ + float v_t_new = v_t_orig + fy_t * (v_b_orig - v_t_orig); + float v_b_new = v_t_orig + fy_b * (v_b_orig - v_t_orig); + + /* Write clipped UVs into a local 4-corner array and + * point the local tex_coord pointer at it. Layout + * matches d3d8_tex_coords: BL, BR, TL, TR. */ + d3d->menu_display.scissor_uv[0] = u_l_new; + d3d->menu_display.scissor_uv[1] = v_b_new; + d3d->menu_display.scissor_uv[2] = u_r_new; + d3d->menu_display.scissor_uv[3] = v_b_new; + d3d->menu_display.scissor_uv[4] = u_l_new; + d3d->menu_display.scissor_uv[5] = v_t_new; + d3d->menu_display.scissor_uv[6] = u_r_new; + d3d->menu_display.scissor_uv[7] = v_t_new; + clipped_uv = d3d->menu_display.scissor_uv; + + /* Now mutate the screen rect to the clipped one. + * Convert new_bot back to bottom-up Y for draw->y. */ + draw->x = new_left; + draw->y = (int)video_height - new_bot; + draw->width = (unsigned)(new_right - new_left); + draw->height = (unsigned)(new_bot - new_top); + } + } + } + if ((d3d->menu_display.offset + draw->coords->vertices ) > (unsigned)d3d->menu_display.size) return; @@ -710,22 +879,39 @@ static void gfx_display_d3d8_draw(gfx_display_ctx_draw_t *draw, pv += d3d->menu_display.offset; vertex = draw->coords->vertex; - tex_coord = draw->coords->tex_coord; + tex_coord = clipped_uv ? clipped_uv : draw->coords->tex_coord; color = draw->coords->color; if (!vertex) vertex = &d3d8_vertexes[0]; if (!tex_coord) tex_coord = &d3d8_tex_coords[0]; + if (!color) + { + /* Default to opaque white when caller provides no color + * array — matches the behaviour of the d3d9/d3d10/d3d11 + * gfx_display drivers and avoids dereferencing NULL on + * pipeline/dispca-driven draws. */ + static const float default_color[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; + color = &default_color[0]; + } for (i = 0; i < draw->coords->vertices; i++) { int colors[4]; + const float *cp = color; - colors[0] = *color++ * 0xFF; - colors[1] = *color++ * 0xFF; - colors[2] = *color++ * 0xFF; - colors[3] = *color++ * 0xFF; + colors[0] = *cp++ * 0xFF; + colors[1] = *cp++ * 0xFF; + colors[2] = *cp++ * 0xFF; + colors[3] = *cp++ * 0xFF; + + /* Advance the color pointer only when the caller actually + * provided a per-vertex color array; if we fell back to the + * static default above, reuse that single RGBA for every + * vertex. */ + if (draw->coords->color) + color = cp; pv[i].x = *vertex++; pv[i].y = *vertex++; @@ -733,18 +919,6 @@ static void gfx_display_d3d8_draw(gfx_display_ctx_draw_t *draw, pv[i].u = *tex_coord++; pv[i].v = *tex_coord++; - if ((void*)draw->texture) - { - D3DSURFACE_DESC desc; - LPDIRECT3DTEXTURE8 tex = (LPDIRECT3DTEXTURE8)draw->texture; - if (SUCCEEDED(IDirect3DTexture8_GetLevelDesc(tex, - 0, (D3DSURFACE_DESC*)&desc))) - { - pv[i].u *= desc.Width; - pv[i].v *= desc.Height; - } - } - pv[i].color = D3DCOLOR_ARGB( colors[3], /* A */ @@ -791,12 +965,68 @@ static void gfx_display_d3d8_draw(gfx_display_ctx_draw_t *draw, IDirect3DDevice8_SetTextureStageState(dev, 0, (D3DTEXTURESTAGESTATETYPE)D3DTSS_MAGFILTER, D3DTEXF_LINEAR); } + else + { + /* Untextured draw — clear any stale texture binding (the + * font atlas left bound after font_driver_render_msg, the + * libretro frame texture from d3d8_render, etc.) so the + * default texture-stage MODULATE doesn't multiply the + * per-vertex DIFFUSE colour against an unrelated sample. + * Without this, divider lines and selection highlights in + * Ozone/XMB/MaterialUI would pick up whatever texture was + * last bound and render as garbage or invisibly. */ + IDirect3DDevice8_SetTexture(dev, 0, NULL); + } + + /* Force the alpha pipeline to MODULATE(TEXTURE, DIFFUSE). + * + * The fixed-function default for stage 0 is + * COLOROP = MODULATE, COLORARG1 = TEXTURE, COLORARG2 = CURRENT + * ALPHAOP = SELECTARG1, ALPHAARG1 = TEXTURE + * which means the colour channel correctly multiplies the + * texture sample by the per-vertex DIFFUSE colour, but the + * alpha channel ignores DIFFUSE entirely and just selects the + * texture's alpha. For an opaque texture (the common case — + * gfx_white_texture, icon atlases, the Ozone cursor texture) + * that means a draw whose only opacity comes from per-vertex + * alpha (e.g. a fading-out "old" cursor at alpha=0, or a + * semi-transparent footer fill) renders fully opaque instead + * of fading. + * + * Switching ALPHAOP to MODULATE makes the alpha output equal + * texture.alpha * diffuse.alpha, which is what every menu + * caller expects (and what the d3d9/d3d10/d3d11 stock shaders + * compute explicitly). COLOROP stays at its default. */ + IDirect3DDevice8_SetTextureStageState(dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAOP, D3DTOP_MODULATE); + IDirect3DDevice8_SetTextureStageState(dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + IDirect3DDevice8_SetTextureStageState(dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAARG2, D3DTA_DIFFUSE); type = gfx_display_prim_to_d3d8_enum(draw->prim_type); start = d3d->menu_display.offset; - count = draw->coords->vertices - - ((draw->prim_type == GFX_DISPLAY_PRIM_TRIANGLESTRIP) - ? 2 : 0); + + /* For tristrips, the primitive count is vertices - 2. Guard + * against vertices < 3 which would underflow the unsigned + * subtraction and pass a huge primitive count to the GPU. */ + if (draw->prim_type == GFX_DISPLAY_PRIM_TRIANGLESTRIP) + { + if (draw->coords->vertices < 3) + { + d3d->menu_display.offset += draw->coords->vertices; + return; + } + count = draw->coords->vertices - 2; + } + else + count = draw->coords->vertices; + + if (count == 0) + { + d3d->menu_display.offset += draw->coords->vertices; + return; + } IDirect3DDevice8_BeginScene(dev); IDirect3DDevice8_DrawPrimitive(dev, type, start, count); @@ -805,9 +1035,716 @@ static void gfx_display_d3d8_draw(gfx_display_ctx_draw_t *draw, d3d->menu_display.offset += draw->coords->vertices; } +/* Set up render state for one of the menu pipeline draws (XMB + * ribbon backgrounds, snow/bokeh particle effects, etc.). + * + * D3D8 has no programmable shader path here — the actual menu + * shaders the other backends compile (ribbon_sm3, simple_snow_sm3, + * snowflake_sm3, bokeh_sm3) need at minimum pixel shader 2.0 to + * fit; PS 1.x cannot represent the per-fragment noise math. So + * for d3d8 we deliberately skip programmable shading entirely and + * only do the work that *can* be done in fixed function: + * + * - Hand the dispca coordinate array to the caller via + * draw->coords so the subsequent gfx_display_d3d8_draw call + * has geometry to render. + * - Set the per-pipeline blend mode so the geometry composites + * against the background the way XMB expects (ribbon uses + * multiplicative DESTCOLOR+ONE, particle effects use the + * usual SRCALPHA / INVSRCALPHA premultiplied path). + * + * The result is that the ribbon and particle layers render as + * static geometry rather than the animated shader effect — the + * menu still composes correctly, just without the eye-candy. + */ +static void gfx_display_d3d8_draw_pipeline( + gfx_display_ctx_draw_t *draw, + gfx_display_t *p_disp, + void *data, unsigned video_width, unsigned video_height) +{ + video_coord_array_t *ca; + d3d8_video_t *d3d = (d3d8_video_t*)data; + + if (!d3d || !draw || !p_disp) + return; + + ca = &p_disp->dispca; + + /* Position the geometry at the origin and clear any inherited + * MVP — gfx_display_d3d8_draw will fall back to identity. */ + draw->x = 0; + draw->y = 0; + draw->matrix_data = NULL; + + if (ca) + draw->coords = (struct video_coords*)&ca->coords; + + switch (draw->pipeline_id) + { + case VIDEO_SHADER_MENU: + case VIDEO_SHADER_MENU_2: + /* XMB ribbon: multiplicative blend so the ribbon mesh + * darkens / tints whatever is behind it. Matches the + * blend setup d3d10/d3d11 use for the ribbon pass. */ + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_DESTBLEND, D3DBLEND_ONE); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_ALPHABLENDENABLE, TRUE); + break; + + case VIDEO_SHADER_MENU_3: + case VIDEO_SHADER_MENU_4: + case VIDEO_SHADER_MENU_5: + case VIDEO_SHADER_MENU_6: + /* Snow / bokeh / snowflake: standard alpha blend. The + * dispca geometry alone won't produce a particle effect + * without the pixel shader, but at least the blend mode + * is consistent so any text/icons drawn afterwards don't + * inherit a stale state. */ + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_ALPHABLENDENABLE, TRUE); + break; + + default: + /* Unknown pipeline ID — leave blend state alone and let + * the regular draw path render whatever was set up. */ + break; + } +} + +/* Soft scissor for D3D8. + * + * D3D8 has no SetScissorRect (added in D3D9) and no + * D3DRS_SCISSORTESTENABLE. Two workarounds are possible: + * + * 1. Shrink the viewport to the requested rect. This does not + * clip — it transforms full-screen geometry into the smaller + * rect — so it produces visibly squashed text/icons when + * callers (e.g. Ozone's sidebar pass) draw at full-screen + * coordinates expecting clipping. + * + * 2. Software clipping in gfx_display_d3d8_draw — skip any draw + * whose screen-space bounding box lies entirely outside the + * requested rect. This is partial — partially-overlapping + * draws still render in full — but it stops fully-outside + * draws (the entry-list overflow that spills onto Ozone's + * footer) which is the visible artifact users actually + * notice. True geometry clipping (per-vertex remap with UV + * adjustment) would be invasive to do for every quad and is + * not worth the complexity for a fallback-quality backend. + * + * scissor_begin stores the rect; scissor_end clears it; the draw + * function consults the rect when active. */ +static void gfx_display_d3d8_scissor_begin( + void *data, + unsigned video_width, unsigned video_height, + int x, int y, unsigned width, unsigned height) +{ + d3d8_video_t *d3d = (d3d8_video_t*)data; + + if (!d3d) + return; + + d3d->menu_display.scissor_x = x; + d3d->menu_display.scissor_y = y; + d3d->menu_display.scissor_w = (int)width; + d3d->menu_display.scissor_h = (int)height; + d3d->menu_display.scissor_active = true; +} + +static void gfx_display_d3d8_scissor_end(void *data, + unsigned video_width, unsigned video_height) +{ + d3d8_video_t *d3d = (d3d8_video_t*)data; + + if (!d3d) + return; + + d3d->menu_display.scissor_active = false; +} + +/* + * FONT DRIVER + * + * Fixed-function font renderer for D3D8. Mirrors the structure of + * d3d9_font in d3d9hlsl.c but uses the D3D8 texture-stage-state + * APIs (D3D8 has no programmable shaders or sampler-state objects) + * and an FVF instead of a vertex declaration. The atlas is an A8 + * buffer that we expand to A8R8G8B8 with white RGB; the per-glyph + * tint comes from the per-vertex DIFFUSE colour, modulated by the + * texture sample in the default fixed-function combiner. + */ + +typedef struct +{ + LPDIRECT3DTEXTURE8 texture; + const font_renderer_driver_t *font_driver; + void *font_data; + struct font_atlas *atlas; + unsigned tex_width; + unsigned tex_height; + /* Scratch buffer to avoid per-line malloc/free in font rendering. */ + Vertex *scratch_verts; + unsigned scratch_capacity; /* in Vertex count */ +} d3d8_font_t; + +static void d3d8_font_upload_atlas(d3d8_font_t *font) +{ + D3DLOCKED_RECT lr; + unsigned i, j; + + if (!font->texture) + return; + + if (FAILED(IDirect3DTexture8_LockRect(font->texture, 0, &lr, NULL, 0))) + return; + + for (j = 0; j < font->atlas->height; j++) + { + uint32_t *dst = (uint32_t*)((uint8_t*)lr.pBits + j * lr.Pitch); + const uint8_t *src = font->atlas->buffer + j * font->atlas->width; + for (i = 0; i < font->atlas->width; i++) + dst[i] = D3DCOLOR_ARGB(src[i], 0xFF, 0xFF, 0xFF); + } + + IDirect3DTexture8_UnlockRect(font->texture, 0); +} + +static void *d3d8_font_init(void *data, + const char *font_path, float font_size, + bool is_threaded) +{ + d3d8_video_t *d3d = (d3d8_video_t*)data; + d3d8_font_t *font = (d3d8_font_t*)calloc(1, sizeof(*font)); + + if (!font) + return NULL; + + if (!font_renderer_create_default( + &font->font_driver, &font->font_data, + font_path, font_size)) + { + free(font); + return NULL; + } + + font->atlas = font->font_driver->get_atlas(font->font_data); + font->tex_width = font->atlas->width; + font->tex_height = font->atlas->height; + + /* D3D8 doesn't universally support D3DFMT_A8 as a texture format, + * so expand the A8 atlas into A8R8G8B8 (white RGB, alpha = atlas + * sample). The colour modulation against the per-vertex diffuse + * is done by the default fixed-function texture stage state. */ + font->texture = (LPDIRECT3DTEXTURE8)d3d8_texture_new(d3d->dev, + font->tex_width, font->tex_height, 1, + 0, D3D8_ARGB8888_FORMAT, + D3DPOOL_MANAGED, 0, 0, 0, NULL, NULL, false); + + if (font->texture) + d3d8_font_upload_atlas(font); + + font->atlas->dirty = false; + return font; +} + +static void d3d8_font_free(void *data, bool is_threaded) +{ + d3d8_font_t *font = (d3d8_font_t*)data; + + if (!font) + return; + + if (font->font_driver && font->font_data) + font->font_driver->free(font->font_data); + + if (font->texture) + IDirect3DTexture8_Release(font->texture); + + free(font->scratch_verts); + free(font); +} + +static int d3d8_font_get_message_width(void *data, + const char *msg, size_t msg_len, float scale) +{ + size_t i; + int delta_x = 0; + const struct font_glyph *glyph_q = NULL; + d3d8_font_t *font = (d3d8_font_t*)data; + + if (!font) + return 0; + + glyph_q = font->font_driver->get_glyph(font->font_data, '?'); + + for (i = 0; i < msg_len; i++) + { + const struct font_glyph *glyph; + const char *msg_tmp = &msg[i]; + unsigned code = utf8_walk(&msg_tmp); + unsigned skip = msg_tmp - &msg[i]; + + if (skip > 1) + i += skip - 1; + + if (!(glyph = font->font_driver->get_glyph(font->font_data, code))) + if (!(glyph = glyph_q)) + continue; + + delta_x += glyph->advance_x; + } + + return delta_x * scale; +} + +/* Emit a single glyph quad (6 vertices, two triangles) into pv. + * Returns the number of vertices written (always 6). */ +static INLINE unsigned d3d8_font_emit_quad( + Vertex *pv, + float x, float y, float w, float h, + float tex_u, float tex_v, float tex_w, float tex_h, + D3DCOLOR color) +{ + pv[0].x = x; + pv[0].y = y; + pv[0].z = 0.5f; + pv[0].u = tex_u; + pv[0].v = tex_v; + pv[0].color = color; + + pv[1].x = x + w; + pv[1].y = y; + pv[1].z = 0.5f; + pv[1].u = tex_u + tex_w; + pv[1].v = tex_v; + pv[1].color = color; + + pv[2].x = x; + pv[2].y = y + h; + pv[2].z = 0.5f; + pv[2].u = tex_u; + pv[2].v = tex_v + tex_h; + pv[2].color = color; + + pv[3].x = x + w; + pv[3].y = y; + pv[3].z = 0.5f; + pv[3].u = tex_u + tex_w; + pv[3].v = tex_v; + pv[3].color = color; + + pv[4].x = x + w; + pv[4].y = y + h; + pv[4].z = 0.5f; + pv[4].u = tex_u + tex_w; + pv[4].v = tex_v + tex_h; + pv[4].color = color; + + pv[5].x = x; + pv[5].y = y + h; + pv[5].z = 0.5f; + pv[5].u = tex_u; + pv[5].v = tex_v + tex_h; + pv[5].color = color; + + return 6; +} + +static INLINE Vertex *d3d8_font_get_scratch( + d3d8_font_t *font, unsigned needed) +{ + if (needed > font->scratch_capacity) + { + unsigned new_cap = needed > 1536 ? needed : 1536; /* 256 glyphs * 6 verts */ + Vertex *tmp = (Vertex*)realloc(font->scratch_verts, + new_cap * sizeof(Vertex)); + if (!tmp) + return NULL; + font->scratch_verts = tmp; + font->scratch_capacity = new_cap; + } + memset(font->scratch_verts, 0, needed * sizeof(Vertex)); + return font->scratch_verts; +} + +/* Render a single line of glyphs from `m` of length `msg_len` at + * (line_x, line_y) in [0..1] coords, with the supplied colour. + * Used for both the drop-shadow pass and the main text pass. */ +static void d3d8_font_render_line( + d3d8_video_t *d3d, + d3d8_font_t *font, + const char *m, + size_t msg_len, + float line_x, + float line_y, + float scale, + enum text_alignment text_align, + unsigned width, + unsigned height, + D3DCOLOR color) +{ + unsigned i; + float inv_viewport_w = 1.0f / (float)width; + float inv_viewport_h = 1.0f / (float)height; + float inv_tex_w = 1.0f / (float)font->tex_width; + float inv_tex_h = 1.0f / (float)font->tex_height; + const struct font_glyph *glyph_q = font->font_driver->get_glyph( + font->font_data, '?'); + int lx = roundf(line_x * width); + int ly = roundf((1.0f - line_y) * height); + unsigned vert_count = 0; + Vertex *verts = d3d8_font_get_scratch(font, msg_len * 6); + + if (!verts) + return; + + /* Soft scissor for text. The font path doesn't go through + * gfx_display_d3d8_draw, so apply the same skip-only check + * here. ly is the baseline in top-down screen pixels; + * visible glyphs sit at and above ly (descenders may dip + * slightly below). Conservative cull when the baseline is + * well below the scissor's bottom edge — that's the case + * which produces visible overflow into Ozone's footer. We + * deliberately don't cull on the top side: line height isn't + * known here and the symmetric overflow (entries above the + * scissor) doesn't occur in practice. */ + if (d3d->menu_display.scissor_active) + { + int sy2 = d3d->menu_display.scissor_y + + d3d->menu_display.scissor_h; + if (ly >= sy2) + return; + } + + if (text_align == TEXT_ALIGN_RIGHT || text_align == TEXT_ALIGN_CENTER) + { + int width_accum = 0; + const char *scan = m; + const char *scan_end = m + msg_len; + while (scan < scan_end) + { + const struct font_glyph *glyph; + uint32_t code = utf8_walk(&scan); + if (!(glyph = font->font_driver->get_glyph(font->font_data, code))) + if (!(glyph = glyph_q)) + continue; + width_accum += glyph->advance_x; + } + if (text_align == TEXT_ALIGN_RIGHT) + line_x -= (float)(width_accum * scale) / (float)width; + else + line_x -= (float)(width_accum * scale) / (float)width / 2.0f; + lx = roundf(line_x * width); + } + + for (i = 0; i < msg_len; i++) + { + const struct font_glyph *glyph; + const char *msg_tmp = &m[i]; + unsigned code = utf8_walk(&msg_tmp); + unsigned skip = msg_tmp - &m[i]; + + if (skip > 1) + i += skip - 1; + + if (!(glyph = font->font_driver->get_glyph(font->font_data, code))) + if (!(glyph = glyph_q)) + continue; + + vert_count += d3d8_font_emit_quad( + &verts[vert_count], + (lx + glyph->draw_offset_x * scale) * inv_viewport_w, + (ly + glyph->draw_offset_y * scale) * inv_viewport_h, + glyph->width * scale * inv_viewport_w, + glyph->height * scale * inv_viewport_h, + glyph->atlas_offset_x * inv_tex_w, + glyph->atlas_offset_y * inv_tex_h, + glyph->width * inv_tex_w, + glyph->height * inv_tex_h, + color); + + lx += glyph->advance_x * scale; + ly += glyph->advance_y * scale; + } + + if (vert_count == 0) + return; + + IDirect3DDevice8_SetTexture(d3d->dev, 0, + (IDirect3DBaseTexture8*)font->texture); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ADDRESSU, D3DTADDRESS_CLAMP); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ADDRESSV, D3DTADDRESS_CLAMP); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_MINFILTER, D3DTEXF_LINEAR); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_MAGFILTER, D3DTEXF_LINEAR); + + /* MODULATE the atlas alpha with the per-vertex DIFFUSE alpha + * so callers can fade glyphs in/out via the colour parameter + * (drop-shadow alpha, animation fades, etc). Without this the + * default ALPHAOP=SELECTARG1+TEXTURE makes glyph alpha equal + * the atlas coverage only, ignoring the requested fade. */ + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAOP, D3DTOP_MODULATE); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + IDirect3DDevice8_SetTextureStageState(d3d->dev, 0, + (D3DTEXTURESTAGESTATETYPE)D3DTSS_ALPHAARG2, D3DTA_DIFFUSE); + + IDirect3DDevice8_BeginScene(d3d->dev); + IDirect3DDevice8_DrawPrimitiveUP(d3d->dev, + D3DPT_TRIANGLELIST, + vert_count / 3, + verts, + sizeof(Vertex)); + IDirect3DDevice8_EndScene(d3d->dev); +} + +static void d3d8_font_render_msg( + void *userdata, void *data, + const char *msg, + const struct font_params *params) +{ + float x, y, scale, drop_mod, drop_alpha; + enum text_alignment text_align; + int drop_x, drop_y; + unsigned r, g, b, alpha; + D3DCOLOR color, color_dark = 0; + struct font_line_metrics *line_metrics = NULL; + float line_height; + d3d8_font_t *font = (d3d8_font_t*)data; + d3d8_video_t *d3d = (d3d8_video_t*)userdata; + unsigned width = 0; + unsigned height = 0; + /* Top-down ortho mapping x[0..1]→[-1..1], y[0..1]→[1..-1] so + * (0,0) is the top-left corner. D3D uses row-vector convention + * (v_clip = v · M) and stores D3DMATRIX in row-major order, so + * we write the matrix below in its natural row layout and pass + * it directly to SetTransform (bypassing d3d8_set_mvp which is + * tuned for column-major math_matrix_4x4 input from the menu + * draw path). */ + static const D3DMATRIX topdown_ortho_d3d = { + {{ + 2.0f, 0.0f, 0.0f, 0.0f, /* row 0 */ + 0.0f, -2.0f, 0.0f, 0.0f, /* row 1 */ + 0.0f, 0.0f, 1.0f, 0.0f, /* row 2 */ + -1.0f, 1.0f, 0.0f, 1.0f /* row 3 */ + }} + }; + static const math_matrix_4x4 identity = {{ + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + }}; + + if (!font || !msg || !*msg) + return; + if (!d3d) + return; + + video_driver_get_size(&width, &height); + if (!width || !height) + return; + + if (params) + { + x = params->x; + y = params->y; + scale = params->scale; + text_align = params->text_align; + drop_x = params->drop_x; + drop_y = params->drop_y; + drop_mod = params->drop_mod; + drop_alpha = params->drop_alpha; + + r = FONT_COLOR_GET_RED(params->color); + g = FONT_COLOR_GET_GREEN(params->color); + b = FONT_COLOR_GET_BLUE(params->color); + alpha = FONT_COLOR_GET_ALPHA(params->color); + + color = D3DCOLOR_ARGB(alpha, r, g, b); + } + else + { + settings_t *settings = config_get_ptr(); + float video_msg_pos_x = settings->floats.video_msg_pos_x; + float video_msg_pos_y = settings->floats.video_msg_pos_y; + float video_msg_color_r = settings->floats.video_msg_color_r; + float video_msg_color_g = settings->floats.video_msg_color_g; + float video_msg_color_b = settings->floats.video_msg_color_b; + + x = video_msg_pos_x; + y = video_msg_pos_y; + scale = 1.0f; + text_align = TEXT_ALIGN_LEFT; + + r = (unsigned)(video_msg_color_r * 255); + g = (unsigned)(video_msg_color_g * 255); + b = (unsigned)(video_msg_color_b * 255); + alpha = 255; + color = D3DCOLOR_ARGB(alpha, r, g, b); + + drop_x = -2; + drop_y = -2; + drop_mod = 0.3f; + drop_alpha = 1.0f; + } + + font->font_driver->get_line_metrics(font->font_data, &line_metrics); + line_height = line_metrics->height * scale / height; + + /* Standard premultiplied-alpha blend for glyph compositing. */ + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); + IDirect3DDevice8_SetRenderState(d3d->dev, + D3DRS_ALPHABLENDENABLE, TRUE); + + /* FVF is shared with the rest of the menu draw path. Set it + * defensively in case a previous stage left a different format + * bound (e.g. the renderchain's vertex format). */ + IDirect3DDevice8_SetVertexShader(d3d->dev, + D3DFVF_XYZ | D3DFVF_TEX1 | D3DFVF_DIFFUSE); + + /* Apply top-down ortho. SetTransform consumes a row-major + * D3DMATRIX directly. PROJ and VIEW are forced to identity so + * the WORLD transform alone produces clip space. */ + IDirect3DDevice8_SetTransform(d3d->dev, D3DTS_PROJECTION, + (D3DMATRIX*)&identity); + IDirect3DDevice8_SetTransform(d3d->dev, D3DTS_VIEW, + (D3DMATRIX*)&identity); + IDirect3DDevice8_SetTransform(d3d->dev, D3DTS_WORLD, + &topdown_ortho_d3d); + + /* Refresh the atlas if the glyph cache has grown or new glyphs + * have been emitted since the last frame. */ + if (font->atlas->dirty) + { + if ( font->atlas->width != font->tex_width + || font->atlas->height != font->tex_height) + { + if (font->texture) + IDirect3DTexture8_Release(font->texture); + + font->tex_width = font->atlas->width; + font->tex_height = font->atlas->height; + font->texture = (LPDIRECT3DTEXTURE8)d3d8_texture_new(d3d->dev, + font->tex_width, font->tex_height, 1, + 0, D3D8_ARGB8888_FORMAT, + D3DPOOL_MANAGED, 0, 0, 0, NULL, NULL, false); + } + + d3d8_font_upload_atlas(font); + font->atlas->dirty = false; + } + + { + int lines = 0; + bool has_drop = drop_x || drop_y; + const char *m = msg; + + if (has_drop) + { + unsigned r_dark = r * drop_mod; + unsigned g_dark = g * drop_mod; + unsigned b_dark = b * drop_mod; + unsigned alpha_dark = alpha * drop_alpha; + color_dark = D3DCOLOR_ARGB(alpha_dark, r_dark, g_dark, b_dark); + } + + for (;;) + { + const char *end = m; + size_t msg_len; + + while (*end && *end != '\n') + end++; + msg_len = (size_t)(end - m); + + if (msg_len > 0) + { + float line_y = y - (float)lines * line_height; + + /* Drop shadow pass. */ + if (has_drop) + { + float drop_pos_x = x + scale * drop_x / (float)width; + float drop_pos_y = line_y + scale * drop_y / (float)height; + d3d8_font_render_line(d3d, font, m, msg_len, + drop_pos_x, drop_pos_y, scale, text_align, + width, height, color_dark); + } + + /* Main text pass. */ + d3d8_font_render_line(d3d, font, m, msg_len, + x, line_y, scale, text_align, + width, height, color); + } + + if (*end != '\n') + break; + m = end + 1; + lines++; + } + } + + /* Restore the menu vertex stream so subsequent gfx_display_d3d8_draw + * calls see the correct buffer. d3d9hlsl does this between every + * DrawPrimitiveUP; on d3d8 we only need it once at the end since + * the sole stream switch is to the UP path. */ + IDirect3DDevice8_SetStreamSource(d3d->dev, 0, + (LPDIRECT3DVERTEXBUFFER8)d3d->menu_display.buffer, + sizeof(Vertex)); +} + +static const struct font_glyph *d3d8_font_get_glyph( + void *data, uint32_t code) +{ + d3d8_font_t *font = (d3d8_font_t*)data; + if (font && font->font_driver) + return font->font_driver->get_glyph( + (void*)font->font_data, code); + return NULL; +} + +static bool d3d8_font_get_line_metrics( + void *data, struct font_line_metrics **metrics) +{ + d3d8_font_t *font = (d3d8_font_t*)data; + if (font && font->font_driver && font->font_data) + { + font->font_driver->get_line_metrics(font->font_data, metrics); + return true; + } + return false; +} + +font_renderer_t d3d8_font = { + d3d8_font_init, + d3d8_font_free, + d3d8_font_render_msg, + "d3d8", + d3d8_font_get_glyph, + NULL, /* bind_block */ + NULL, /* flush */ + d3d8_font_get_message_width, + d3d8_font_get_line_metrics +}; + gfx_display_ctx_driver_t gfx_display_ctx_d3d8 = { gfx_display_d3d8_draw, - NULL, /* draw_pipeline */ + gfx_display_d3d8_draw_pipeline, gfx_display_d3d8_blend_begin, gfx_display_d3d8_blend_end, gfx_display_d3d8_get_default_mvp, @@ -817,8 +1754,8 @@ gfx_display_ctx_driver_t gfx_display_ctx_d3d8 = { GFX_VIDEO_DRIVER_DIRECT3D8, "d3d8", false, - NULL, - NULL + gfx_display_d3d8_scissor_begin, + gfx_display_d3d8_scissor_end }; /* @@ -1983,9 +2920,10 @@ static bool d3d8_frame(void *data, const void *frame, if (msg && *msg) { IDirect3DDevice8_SetViewport(d3d->dev, (D3DVIEWPORT8*)&screen_vp); - IDirect3DDevice8_BeginScene(d3d->dev); + /* d3d8_font_render_msg wraps its own BeginScene/EndScene + * around each DrawPrimitiveUP, matching the per-draw scene + * convention used by gfx_display_d3d8_draw. */ font_driver_render_msg(d3d, msg, NULL, NULL); - IDirect3DDevice8_EndScene(d3d->dev); } video_driver_update_title(NULL); @@ -2263,6 +3201,20 @@ static bool d3d8_has_windowed(void *data) { return false; } static bool d3d8_has_windowed(void *data) { return true; } #endif +#ifdef HAVE_GFX_WIDGETS +/* The gfx_widgets layer (notifications, achievement popups, + * load-progress indicators, etc.) is built on top of the same + * gfx_display_ctx that the menu uses, so once gfx_display is + * implemented the widgets only need a non-NULL hook returning + * true to be enabled. d3d8 has the full gfx_display path now, + * so widgets work transparently. */ +static bool d3d8_gfx_widgets_enabled(void *data) +{ + (void)data; + return true; +} +#endif + video_driver_t video_d3d8 = { d3d8_init, d3d8_frame, @@ -2291,6 +3243,6 @@ video_driver_t video_d3d8 = { NULL, /* shader_load_begin */ NULL, /* shader_load_step */ #ifdef HAVE_GFX_WIDGETS - NULL /* gfx_widgets_enabled */ + d3d8_gfx_widgets_enabled #endif }; diff --git a/gfx/font_driver.c b/gfx/font_driver.c index 4924c832586f..70f65b157c5a 100644 --- a/gfx/font_driver.c +++ b/gfx/font_driver.c @@ -157,6 +157,7 @@ static bool font_init_first( case FONT_DRIVER_RENDER_D3D8_API: { static const font_renderer_t *d3d8_font_backends[] = { + &d3d8_font, NULL }; unsigned i; diff --git a/gfx/font_driver.h b/gfx/font_driver.h index fc1464c841b5..1a43a2ae62fb 100644 --- a/gfx/font_driver.h +++ b/gfx/font_driver.h @@ -149,6 +149,7 @@ extern font_renderer_t ctr_font; extern font_renderer_t wiiu_font; extern font_renderer_t vulkan_raster_font; extern font_renderer_t metal_raster_font; +extern font_renderer_t d3d8_font; extern font_renderer_t d3d9_font; extern font_renderer_t d3d9_cg_font; extern font_renderer_t d3d10_font;