Merge commit '128684b6a3526120aa26394f2bd553f991ee03e8' into pr/medievil-quality-upstream

This commit is contained in:
Matthew Stanley
2026-10-03 23:26:54 -07:00
9 changed files with 287 additions and 22 deletions
+46
View File
@@ -0,0 +1,46 @@
# Owned screen-space masks and HUD
Adaptive world projection needs separate layout rules for cutscene overlays.
Game adapters identify the original producers, wait for their packet writes
to finish, and tag completed packets before GPU submission. The renderer
keeps guest packet bytes unchanged.
The public functions in `runtime/include/mod_plugins.h` are:
| API | Contract |
| --- | --- |
| `psx_mod_tag_screen_mask_quad(prim)` | Extend a flat quad's full-width top/bottom panel or existing vertical side band into the visible reveal. |
| `psx_mod_tag_radial_screen_mask_quad(prim, scale)` | Scale an owned flat/Gouraud quad about the display centre; tag every piece of the transition. |
| `psx_mod_anchor_hud_primitive(prim, anchor)` | Translate left/right HUD by the visible side margin, or preserve centered text with `anchor == 0`. |
| `psx_mod_widescreen_view_x_margin()` | Return the configured visible per-side reveal, excluding culling safety guards. |
`prim` addresses the packet word immediately before the GP0 color/command.
For a compound draw-mode-plus-SPRT glyph packet, pass the word before the
SPRT command, rather than the compound packet's initial P_TAG. Tags bind the
complete command words, retain expanded-RAM identity, expire after two
frames and clear on reset/state load. Reused or modified packets do not
inherit the layout rule.
Centered HUD tags bypass automatic UI and backdrop transforms. They retain
glyph coordinates, UVs and dimensions, while left/right tags translate the
whole widget. This guarded anchor API differs from the older role-tagging
API where role zero clears the tag.
Panel reveal bands are disjoint from the authored quad, preserving animated
height and avoiding repeated blending. Full-frame panels use the existing
single expansion. Slanted quads and interior strips do not acquire panel
coverage. Adapters must establish ownership; screen position alone is not
an ownership test.
Radial scale must be finite and between 1 and 64. Derive it from the
producer's authored geometry and pixel aspect; scale both feathered and
solid rings together. The original transition timer remains authoritative.
At 4:3 there is no reveal transform. The older
`psx_mod_widescreen_x_margin()` returns a culling envelope and includes
safety guards, so it is unsuitable for deriving the visible fade radius.
`runtime/tests/test_ws_screen_mask.c` covers bands, whole-frame handling,
expanded-RAM identity, radial transforms, expiration and packet reuse.
The MediEvil II integration independently checked full-width museum bars
and identical subtitle glyph packets/bounds at 4:3, 16:9, 21:9 and 32:9.
That live evidence does not qualify every game's transitions or HUD.
+3
View File
@@ -512,6 +512,9 @@ if(BUILD_TESTING)
add_test(NAME ws_repeat_rect_test COMMAND ws_repeat_rect_test)
add_executable(ws_screen_mask_test tests/test_ws_screen_mask.c)
target_include_directories(ws_screen_mask_test PRIVATE include)
if(UNIX)
target_link_libraries(ws_screen_mask_test PRIVATE m)
endif()
add_test(NAME ws_screen_mask_test COMMAND ws_screen_mask_test)
add_executable(mod_gpu_dma_aperture_test
+9 -2
View File
@@ -261,6 +261,9 @@ void psx_ws_sprite_tag(struct CPUState* cpu);
* by this; 0 when native-wide is inactive). */
int ws_native_wide_active(void);
int ws_nw_extra(void);
/* Configured per-side reveal, excluding the world-culling safety envelope.
* Available before scene classification so first-frame overlays can fit. */
int gpu_ws_configured_x_reveal(void);
int ws_nw_present_width(void);
/* Display width (native px) the native-wide margins are derived from. */
int gpu_ws_display_width(void);
@@ -442,10 +445,14 @@ void gpu_ws_tag_black_reveal_rect(uint32_t prim);
* reveal margins only. The caller verifies the composite's source period.
* Original UVs, texel density, and canonical VRAM writes are unchanged. */
void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period);
/* Title-identified flat screen mask: extend its off-screen vertical boundary
* to the adaptive edge, preserving the interior opening and canonical image.
/* Title-identified flat screen mask: extend vertical sides or horizontal
* letterbox panels to adaptive edges, preserving the interior opening.
* Packet guarded, cleared on reset/load, inert outside native-wide gameplay. */
void gpu_ws_tag_screen_mask_quad(uint32_t prim);
/* Title-owned flat/Gouraud radial screen mask. Render-only scale about the
* display centre; guest coordinates and timing stay unchanged. Scale >= 1,
* inert at 4:3, packet guarded and reset alongside other transient tags. */
void gpu_ws_tag_radial_screen_mask_quad(uint32_t prim, float scale);
/* Pure emulation-thread predicate for a known native-only effect/scene.
* Nonzero forces native 4:3; zero defers. No GPU calls or guest writes inside.
* Host configuration survives reset/load; classification reads live state. */
+15 -1
View File
@@ -106,8 +106,11 @@ uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size, uint32_t alignment);
*/
int psx_mod_set_main_ram_8mb(int enabled);
/* Current per-side widescreen reveal in native game pixels (zero at 4:3). */
/* World-culling envelope in native game pixels, including safety guards. */
int32_t psx_mod_widescreen_x_margin(void);
/* Configured per-side visible reveal, excluding culling guards; zero at 4:3.
* Use for screen-space layout, including the first frame of a new scene. */
int32_t psx_mod_widescreen_view_x_margin(void);
/* Opt into render-only recovery of saturated horizontal GTE projections in
* native-wide gameplay. Requires exact packet-address/word provenance and
@@ -124,6 +127,17 @@ void psx_mod_set_native_wide_nclip_sites(const uint32_t* addresses,
* compositor translates it by the live reveal, excluding culling guards.
* Guest coordinates, world sprites, and native 4:3 remain unchanged. */
void psx_mod_tag_hud_primitive(uint32_t primitive, int edge);
/* Packet-guarded screen-space anchor: -1 left, +1 right, 0 stays centred.
* Unlike the legacy role tag above, zero explicitly protects centred text
* from automatic layout transforms. Word immediately before the GP0 colour
* command; for a compound E1+SPRT packet this is its E1 word (P_TAG+4). */
void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge);
/* Screen-space masks identified by their title's producer, P_TAG addresses.
* Flat panels extend only their exterior boundaries. Radial flat/Gouraud
* quads scale around the display centre for an aspect-aware iris transition.
* Both services are render-only, word guarded and inert at native 4:3. */
void psx_mod_tag_screen_mask_quad(uint32_t primitive);
void psx_mod_tag_radial_screen_mask_quad(uint32_t primitive, float scale);
/* Exclude a known world packet from screen-space backdrop stretching, even
* if it sorts before the first shaded polygon. Zero clears a recycled tag. */
void psx_mod_tag_world_primitive(uint32_t primitive, int is_world);
+55
View File
@@ -0,0 +1,55 @@
#ifndef PSXRECOMP_WS_RADIAL_SCREEN_MASK_H
#define PSXRECOMP_WS_RADIAL_SCREEN_MASK_H
#include "ws_hud_anchor.h"
#include <math.h>
/* Explicit title-owned screen-space rings, never a world-geometry heuristic.
* Independent packet guards let adjacent solid and feathered ring segments
* share one scale without allowing an arena's next use to inherit the effect. */
#define WS_RADIAL_MASK_TAG_COUNT 256u
typedef struct {
WsHudAnchorTag packet;
float scale;
} WsRadialScreenMaskTag;
static inline void ws_radial_mask_clear(WsRadialScreenMaskTag *tags) {
for (unsigned i=0; i<WS_RADIAL_MASK_TAG_COUNT; ++i) tags[i].packet.used=0;
}
static inline void ws_radial_mask_insert(WsRadialScreenMaskTag *tags,
uint32_t key, const WsPrepassPacketGuard *guard, uint32_t frame, float scale) {
if (!guard || !isfinite(scale) || scale < 1.f || scale > 64.f) return;
unsigned slot=(key >> 2) & (WS_RADIAL_MASK_TAG_COUNT-1u), victim=slot;
for (unsigned i=0; i<WS_HUD_ANCHOR_PROBES; ++i) {
unsigned j=(slot+i)&(WS_RADIAL_MASK_TAG_COUNT-1u);
WsHudAnchorTag *p=&tags[j].packet;
if (!p->used || p->command_addr==key) { victim=j; break; }
if (frame-p->frame > WS_HUD_ANCHOR_FRESH_FRAMES) victim=j;
}
tags[victim].packet=(WsHudAnchorTag){.command_addr=key, .frame=frame,
.used=1, .guard=*guard};
tags[victim].scale=scale;
}
static inline int ws_radial_mask_lookup(const WsRadialScreenMaskTag *tags,
uint32_t key, const uint32_t *words, uint32_t count, uint32_t frame,
float *scale) {
unsigned slot=(key >> 2)&(WS_RADIAL_MASK_TAG_COUNT-1u);
for (unsigned i=0; i<WS_HUD_ANCHOR_PROBES; ++i) {
const WsRadialScreenMaskTag *t=&tags[(slot+i)&(WS_RADIAL_MASK_TAG_COUNT-1u)];
if (!t->packet.used || t->packet.command_addr != key) continue;
if (frame<t->packet.frame || frame-t->packet.frame>WS_HUD_ANCHOR_FRESH_FRAMES ||
!ws_prepass_packet_matches(&t->packet.guard,words,count)) return 0;
*scale=t->scale;
return 1;
}
return 0;
}
static inline void ws_radial_mask_transform(int32_t x[4], int32_t y[4],
int width, int height, float scale) {
if (width<=0 || height<=0 || !isfinite(scale) || scale<1.f || scale>64.f) return;
double cx=width*.5, cy=height*.5;
for (unsigned i=0; i<4; ++i) {
x[i]=(int32_t)llround(cx+(x[i]-cx)*scale);
y[i]=(int32_t)llround(cy+(y[i]-cy)*scale);
}
}
#endif
+27
View File
@@ -24,4 +24,31 @@ static inline int ws_screen_mask_band(const int32_t x[4], const int32_t y[4],
}
return 0;
}
/* A tagged horizontal letterbox panel covers the native width but only the
* top or bottom of the picture. Extend both outer edges, keeping its animated
* height and leaving the opening alone. Full-screen filters use the renderer's
* existing single-rectangle path instead, avoiding a second blend in the sides.
* At most two disjoint bands are returned, all outside canonical VRAM. */
static inline int ws_screen_mask_bands(const int32_t x[4], const int32_t y[4],
int width, int height, int margin,
WsScreenMaskBand out[2]) {
if (!out || width <= 0 || height <= 0 || margin <= 0) return 0;
if (x[0] == x[2] && x[1] == x[3] &&
y[0] == y[1] && y[2] == y[3] &&
x[0] <= 0 && x[1] >= width && y[2] > y[0]) {
if (y[0] <= 0 && y[2] >= height) return 0;
if (y[0] <= 0 || y[2] >= height) {
int count = 0;
if (x[0] > -margin)
out[count++] = (WsScreenMaskBand){-margin, y[0],
margin + x[0], y[2] - y[0]};
if (x[1] < width + margin)
out[count++] = (WsScreenMaskBand){x[1], y[0],
width + margin - x[1], y[2] - y[0]};
return count;
}
}
return ws_screen_mask_band(x, y, width, height, margin, out);
}
#endif
+59 -19
View File
@@ -44,6 +44,7 @@
#include "ws_hud_anchor.h"
#include "ws_repeat_rect.h"
#include "ws_screen_mask.h"
#include "ws_radial_screen_mask.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -192,6 +193,7 @@ static WsTag ws_tags[WS_TAG_BUCKETS];
static WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE];
static WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE];
static WsHudAnchorTag ws_screen_mask_tags[WS_HUD_ANCHOR_TABLE_SIZE];
static WsRadialScreenMaskTag ws_radial_screen_mask_tags[WS_RADIAL_MASK_TAG_COUNT];
static WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE];
static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */
static uint32_t ws_last_3d_stamp = (uint32_t)-1000; /* frame of newest shaded prim (diagnostic) */
@@ -208,6 +210,7 @@ static int ws_engaged(void) { return ws_mode != 0; }
static int32_t ws_scale_about(int32_t x, int32_t ax);
static int32_t ws_disp_x(void);
static int32_t ws_disp_w(void);
static int32_t ws_disp_h(void);
static void ws_clear_all_reveal_margins(void);
/* Gameplay vs full-2D screen. Character/billboard prims tag (psx_ws_sprite_tag)
@@ -545,6 +548,7 @@ static int ws_nw_offset(void) {
if (!ws_native_wide_active()) return 0;
return ws_nw_configured_offset();
}
int gpu_ws_configured_x_reveal(void) { return ws_nw_configured_offset(); }
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
static uint32_t ws_view_camera_addr, ws_view_min_addr, ws_view_max_addr, ws_view_active_addr;
@@ -2325,10 +2329,34 @@ void gpu_ws_tag_screen_mask_quad(uint32_t prim) {
((words[0] >> 24) & 0xfdu) != 0x28u) return;
WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count);
ws_hud_anchor_insert(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
(prim + 4u) & 0x1ffffcu, 0, &guard,
GPU_RAM_KEY(prim + 4u), 0, &guard,
(uint32_t)s_frame_count);
}
void gpu_ws_tag_radial_screen_mask_quad(uint32_t prim, float scale) {
if (!ws_native_wide_configured() || (prim & 3u) || prim > UINT32_MAX - 4u)
return;
uint32_t words[12], count=0;
if (!ws_hud_command_words(prim+4u, words, &count)) return;
unsigned op=words[0]>>24;
if (!((count==5u && (op&0xfdu)==0x28u) ||
(count==8u && (op&0xfdu)==0x38u))) return;
WsPrepassPacketGuard guard=ws_prepass_packet_guard(words,count);
ws_radial_mask_insert(ws_radial_screen_mask_tags,GPU_RAM_KEY(prim+4u),
&guard,(uint32_t)s_frame_count,scale);
}
static int ws_nw_radial_mask_transform(int32_t x[4], int32_t y[4]) {
if (!ws_native_wide_active() || psx_ws_x_margin()<=0 ||
gp0_cmd_source_addr==UINT32_MAX) return 0;
float scale;
if (!ws_radial_mask_lookup(ws_radial_screen_mask_tags,
GPU_RAM_KEY(gp0_cmd_source_addr),gp0_cmd_buf,(uint32_t)gp0_words_needed,
(uint32_t)s_frame_count,&scale)) return 0;
ws_radial_mask_transform(x,y,ws_disp_w(),ws_disp_h(),scale);
return 1;
}
static int ws_nw_explicit_hud_delta(int32_t *out_delta) {
if (out_delta) *out_delta = 0;
if (!ws_native_wide_active() || gp0_cmd_source_addr == 0xFFFFFFFFu)
@@ -2671,7 +2699,7 @@ static int ws_axis_aligned_quad(const int32_t vx[4], const int32_t vy[4]) {
}
static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) {
if (psx_ws_prim_is_tagged() || !ws_axis_aligned_quad(vx, vy))
if (ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged() || !ws_axis_aligned_quad(vx, vy))
return 0;
int32_t min_x = vx[0], max_x = vx[0], min_y = vy[0], max_y = vy[0];
@@ -2696,7 +2724,7 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) {
}
static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) {
if (!x || !w || *w <= 0 || psx_ws_prim_is_tagged())
if (!x || !w || *w <= 0 || ws_nw_explicit_hud_delta(NULL) || psx_ws_prim_is_tagged())
return 0;
int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h();
if ((*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) ||
@@ -2855,7 +2883,7 @@ static int ws_nw_flat_backdrop = 0;
void gpu_ws_set_nw_flat_backdrop(int on) { ws_nw_flat_backdrop = on ? 1 : 0; }
int gpu_ws_nw_flat_backdrop_enabled(void) { return ws_nw_flat_backdrop; }
static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) {
if (!ws_nw_backdrop || !ws_native_wide_active()) return 0;
if (!ws_nw_backdrop || !ws_native_wide_active() || ws_nw_explicit_hud_delta(NULL)) return 0;
int32_t extra = ws_nw_extra();
if (extra <= 0) return 0;
int32_t X = ws_disp_x();
@@ -3303,6 +3331,7 @@ static void gpu_reset_state(int clear_vram) {
ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_radial_mask_clear(ws_radial_screen_mask_tags);
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
polyline_color = 0;
polyline_prev_x = polyline_prev_y = 0;
@@ -4389,20 +4418,25 @@ static void gp0_exec_mono_quad(void) {
int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2);
int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3);
if (rej_a && rej_b) return;
ws_auto_ui_transform_quad(vx, vy);
int radial_mask=ws_nw_radial_mask_transform(vx,vy);
int screen_mask=ws_native_wide_active() && gp0_cmd_source_addr != UINT32_MAX &&
ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
GPU_RAM_KEY(gp0_cmd_source_addr), gp0_cmd_buf, 5u,
(uint32_t)s_frame_count, NULL);
if (!radial_mask && !screen_mask) ws_auto_ui_transform_quad(vx, vy);
/* Each added band is outside the original screen and executes in the
* mask's own OT position/blend mode. Canonical VRAM clips it away. */
if (ws_native_wide_active() && gp0_cmd_source_addr != UINT32_MAX &&
ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
gp0_cmd_source_addr & 0x1ffffcu, gp0_cmd_buf, 5u,
(uint32_t)s_frame_count, NULL)) {
WsScreenMaskBand band;
if (ws_screen_mask_band(vx, vy, ws_disp_w(), ws_disp_h(),
draw_area_wide_x_margin(), &band)) {
if (screen_mask) {
WsScreenMaskBand bands[2];
int count = ws_screen_mask_bands(vx, vy, ws_disp_w(), ws_disp_h(),
draw_area_wide_x_margin(), bands);
if (count) {
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
gr_draw_flat_rect(band.x + draw_offset_x, band.y + draw_offset_y,
band.w, band.h, color);
for (int i = 0; i < count; i++)
gr_draw_flat_rect(bands[i].x + draw_offset_x,
bands[i].y + draw_offset_y,
bands[i].w, bands[i].h, color);
}
}
@@ -4427,8 +4461,10 @@ static void gp0_exec_mono_quad(void) {
gr_draw_flat_rect(x, y, w, h, color);
return;
}
ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (no-op else) */
ws_nw_hud_shift_vertices(vx, 4);
if (!radial_mask && !screen_mask) {
ws_nw_backdrop_stretch_quad(vx, vy);
ws_nw_hud_shift_vertices(vx, 4);
}
for (int i = 0; i < 4; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
@@ -4512,9 +4548,12 @@ static void gp0_exec_shaded_quad(void) {
int rej_a = psx_gpu_triangle_oversize(vx, vy, 0, 1, 2);
int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3);
if (rej_a && rej_b) return;
ws_auto_ui_transform_quad(vx, vy);
ws_nw_backdrop_stretch_quad(vx, vy); /* full-frame 2D backdrop stretch (sky gradient; no-op else) */
ws_nw_hud_shift_vertices(vx, 4);
int radial_mask=ws_nw_radial_mask_transform(vx,vy);
if (!radial_mask) {
ws_auto_ui_transform_quad(vx, vy);
ws_nw_backdrop_stretch_quad(vx, vy);
ws_nw_hud_shift_vertices(vx, 4);
}
for (int i = 0; i < 4; i++) {
vx[i] += draw_offset_x;
vy[i] += draw_offset_y;
@@ -6953,6 +6992,7 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
ws_radial_mask_clear(ws_radial_screen_mask_tags);
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
/* Sync renderer clip/scissor to restored GP0(E3/E4); vars alone leave GL
* on a stale draw area after savestate load. */
+12
View File
@@ -1526,10 +1526,22 @@ extern "C" uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size,
extern "C" int32_t psx_mod_widescreen_x_margin(void) {
return (int32_t)psx_ws_x_margin();
}
extern "C" int32_t psx_mod_widescreen_view_x_margin(void) {
return (int32_t)gpu_ws_configured_x_reveal();
}
extern "C" void psx_mod_tag_hud_primitive(uint32_t primitive, int edge) {
gpu_ws_tag_hud_primitive(primitive, edge);
}
extern "C" void psx_mod_anchor_hud_primitive(uint32_t primitive, int edge) {
gpu_ws_tag_hud_prim(primitive, edge);
}
extern "C" void psx_mod_tag_screen_mask_quad(uint32_t primitive) {
gpu_ws_tag_screen_mask_quad(primitive);
}
extern "C" void psx_mod_tag_radial_screen_mask_quad(uint32_t primitive, float scale) {
gpu_ws_tag_radial_screen_mask_quad(primitive, scale);
}
extern "C" void psx_mod_tag_world_primitive(uint32_t primitive, int is_world) {
gpu_ws_tag_world_primitive(primitive, is_world);
+61
View File
@@ -4,6 +4,7 @@
#include <assert.h>
#include "ws_screen_mask.h"
#include "ws_hud_anchor.h"
#include "ws_radial_screen_mask.h"
int main(void) {
int32_t x[4]={-96,160,-96,68},y[4]={0,0,240,240};
WsScreenMaskBand b;
@@ -17,13 +18,73 @@ int main(void) {
assert(b.x==416 && b.w==598);
x[3]=415; assert(!ws_screen_mask_band(x,y,320,240,694,&b));
x[3]=416;y[3]=239;assert(!ws_screen_mask_band(x,y,320,240,694,&b));
WsScreenMaskBand bands[2];
x[0]=x[2]=0; x[1]=x[3]=512;
y[0]=y[1]=0; y[2]=y[3]=25;
for (int margin=0; margin<16000; margin+=53) {
int count=ws_screen_mask_bands(x,y,512,240,margin,bands);
assert(count==(margin ? 2 : 0));
if (count) {
assert(bands[0].x==-margin && bands[0].w==margin);
assert(bands[1].x==512 && bands[1].w==margin);
assert(bands[0].y==0 && bands[0].h==25);
assert(bands[1].y==0 && bands[1].h==25);
}
}
y[0]=y[1]=215; y[2]=y[3]=240;
assert(ws_screen_mask_bands(x,y,512,240,170,bands)==2);
assert(bands[0].y==215 && bands[0].h==25);
/* Already extended edges need no second draw; one-sided overlap is safe. */
x[0]=x[2]=-200; x[1]=x[3]=600;
assert(ws_screen_mask_bands(x,y,512,240,170,bands)==1);
assert(bands[0].x==600 && bands[0].w==82);
x[1]=x[3]=700;
assert(!ws_screen_mask_bands(x,y,512,240,170,bands));
/* A world quad, middle strip, empty panel or full-screen blend stays alone. */
x[0]=x[2]=0; x[1]=x[3]=512;
y[0]=y[1]=20; y[2]=y[3]=40;
assert(!ws_screen_mask_bands(x,y,512,240,170,bands));
y[0]=y[1]=0; y[2]=y[3]=240;
assert(!ws_screen_mask_bands(x,y,512,240,170,bands));
y[2]=y[3]=0;
assert(!ws_screen_mask_bands(x,y,512,240,170,bands));
y[2]=y[3]=25; x[3]=511;
assert(!ws_screen_mask_bands(x,y,512,240,170,bands));
/* Same packet address with different geometry/colour must lose its tag. */
WsHudAnchorTag tags[WS_HUD_ANCHOR_TABLE_SIZE]={0};
uint32_t words[5]={0x2a7b7b7b,0x0000ffa0,0xa0,0xf0ffa0,0xf00044};
WsPrepassPacketGuard guard=ws_prepass_packet_guard(words,5);
ws_hud_anchor_insert(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,0,&guard,20);
assert(ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
/* Expanded render memory must not alias the same packet offset at 2 MiB. */
assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0x2a68b4,words,5,20,0));
ws_hud_anchor_insert(tags,WS_HUD_ANCHOR_TABLE_SIZE,0x2a68b4,0,&guard,20);
assert(ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0x2a68b4,words,5,20,0));
assert(ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
words[2]++;assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
words[2]--;ws_hud_anchor_clear(tags,WS_HUD_ANCHOR_TABLE_SIZE);
assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
WsRadialScreenMaskTag radial[WS_RADIAL_MASK_TAG_COUNT]={0};
float scale=0;
ws_radial_mask_insert(radial,0x4a68b4,&guard,20,1.5f);
assert(ws_radial_mask_lookup(radial,0x4a68b4,words,5,22,&scale));
assert(scale==1.5f);
assert(!ws_radial_mask_lookup(radial,0x2a68b4,words,5,22,&scale));
assert(!ws_radial_mask_lookup(radial,0x4a68b4,words,5,23,&scale));
words[1]++;
assert(!ws_radial_mask_lookup(radial,0x4a68b4,words,5,20,&scale));
words[1]--;
ws_radial_mask_insert(radial,0x4a68b4,&guard,20,NAN);
assert(ws_radial_mask_lookup(radial,0x4a68b4,words,5,20,&scale));
assert(scale==1.5f);
int32_t rx[4]={256,356,-128,640}, ry[4]={120,170,-72,312};
ws_radial_mask_transform(rx,ry,512,240,1.f);
assert(rx[1]==356 && ry[1]==170);
ws_radial_mask_transform(rx,ry,512,240,1.5f);
assert(rx[0]==256 && ry[0]==120);
assert(rx[1]==406 && ry[1]==195);
assert(rx[2]==-320 && ry[2]==-168);
assert(rx[3]==832 && ry[3]==408);
ws_radial_mask_clear(radial);
assert(!ws_radial_mask_lookup(radial,0x4a68b4,words,5,20,&scale));
}