Support explicitly owned adaptive screen masks and centred HUD packets

This commit is contained in:
Matthew Stanley
2026-10-02 20:37:29 -07:00
parent 6b9a2d49ac
commit 977c25dba3
8 changed files with 230 additions and 21 deletions
+3
View File
@@ -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
+6 -2
View File
@@ -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. */
+11
View File
@@ -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);
+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
+58 -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)
@@ -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. */
+9
View File
@@ -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);
+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));
}