diff --git a/Makefile.common b/Makefile.common index a0ad16c9366..97af05f52da 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/configuration.c b/configuration.c index dd9082bf934..a2243c9a7a1 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/common/d3d9_common.c b/gfx/common/d3d9_common.c index 3e6e5a6f656..4a338418c51 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/common/dx_guids.c b/gfx/common/dx_guids.c index bab2d06b9e8..483bc7f144a 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 00000000000..36742f5d6f2 --- /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 00000000000..4c751405d77 --- /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 4424618e7ae..17907facc71 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" @@ -79,10 +81,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 @@ -108,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; @@ -128,6 +152,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 @@ -215,8 +247,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. 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; - 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 @@ -668,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; @@ -683,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++; @@ -706,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 */ @@ -764,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); @@ -778,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, @@ -790,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 }; /* @@ -825,7 +1789,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; @@ -1387,17 +2351,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 ? @@ -1953,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); @@ -1980,17 +2948,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) @@ -1998,6 +2974,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; @@ -2011,7 +2988,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) { @@ -2027,24 +3004,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); } } @@ -2225,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, @@ -2253,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 4924c832586..70f65b157c5 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 fc1464c841b..1a43a2ae62f 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; diff --git a/menu/drivers/rgui.c b/menu/drivers/rgui.c index 0744f416c6d..be94f6a0949 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;