diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 56a208d9..b0b860c2 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -469,6 +469,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 diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 75c30d66..b4191114 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -429,10 +429,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. */ diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 2bb49d20..b83950eb 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -114,6 +114,17 @@ int32_t psx_mod_widescreen_x_margin(void); * 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); diff --git a/runtime/include/ws_radial_screen_mask.h b/runtime/include/ws_radial_screen_mask.h new file mode 100644 index 00000000..f2b103ea --- /dev/null +++ b/runtime/include/ws_radial_screen_mask.h @@ -0,0 +1,55 @@ +#ifndef PSXRECOMP_WS_RADIAL_SCREEN_MASK_H +#define PSXRECOMP_WS_RADIAL_SCREEN_MASK_H +#include "ws_hud_anchor.h" +#include + +/* 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 64.f) return; + unsigned slot=(key >> 2) & (WS_RADIAL_MASK_TAG_COUNT-1u), victim=slot; + for (unsigned i=0; iused || 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; ipacket.used || t->packet.command_addr != key) continue; + if (framepacket.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 diff --git a/runtime/include/ws_screen_mask.h b/runtime/include/ws_screen_mask.h index 9e228bf8..8d11a152 100644 --- a/runtime/include/ws_screen_mask.h +++ b/runtime/include/ws_screen_mask.h @@ -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 diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 6f0236ae..c2e15fe3 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -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 #include #include @@ -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) @@ -2251,10 +2254,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) @@ -2597,7 +2624,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]; @@ -2622,7 +2649,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) || @@ -2781,7 +2808,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(); @@ -3229,6 +3256,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; @@ -4235,20 +4263,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); } } @@ -4273,8 +4306,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; @@ -4358,9 +4393,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; @@ -6788,6 +6826,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. */ diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 2c6e4a6a..91478239 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1530,6 +1530,15 @@ extern "C" int32_t psx_mod_widescreen_x_margin(void) { 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); diff --git a/runtime/tests/test_ws_screen_mask.c b/runtime/tests/test_ws_screen_mask.c index b8571340..f8379bb3 100644 --- a/runtime/tests/test_ws_screen_mask.c +++ b/runtime/tests/test_ws_screen_mask.c @@ -4,6 +4,7 @@ #include #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)); }