diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 27d956f7..4986a09b 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,8 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-10-21 (BRIEF-4 full-range work):** Reuse exact indexed 1555 page decoding and epoch lookup from `ab784091`/`341a3598`, restricted to raw VRAM-backed source/palette to avoid mutable bank-name reuse. Coherency writes and transaction restoration invalidate affected regions. Skip debug-only NCLIP hash-table receipts in production; retain culling math and summary counters. R4 failing quiet/loaded acceptance cases will be repeated after these fixes. + - **2026-10-21 (BRIEF-4 far projection):** Opt-in preserve-projection can recover IR-saturated X/Y from qualified unshifted MACs. A new intact-word tag requires validated GTE dataflow; modified words, half writes, cache fallback, lm sign clamps and accumulator/SZ overflow keep rejection. Guest registers and the faithful default are unchanged. R4 maximum-range visual/performance acceptance remains pending. - **2026-10-21 (BRIEF-4 production follow-up):** Remove production-only PGXP diagnostic receipt copies and reuse the FLS-owned Windows sleep timer fix from `2ca004ba`. The PGXP quiet repeat reduces p99 busy from 70.6% to 50.5%, but two >25ms completion gaps remain. Only failing acceptance runs are repeated under the owner minimum-testing rule. diff --git a/runtime/src/gl_texel_cache.inc b/runtime/src/gl_texel_cache.inc new file mode 100644 index 00000000..4196c64f --- /dev/null +++ b/runtime/src/gl_texel_cache.inc @@ -0,0 +1,215 @@ +/* Exact indexed-page decode cache. Included after TEX_FS and batch helpers. + * Array layers contain native 1555 words, not filtered/display colours. */ +#ifndef GL_TEXTURE_2D_ARRAY +#define GL_TEXTURE_2D_ARRAY 0x8C1A +#endif +typedef void (APIENTRY *TcImage3D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLsizei, + GLint, GLenum, GLenum, const void *); +typedef void (APIENTRY *TcAttachLayer)(GLenum, GLenum, GLuint, GLint, GLint); +#define TC_CAP 256 +typedef struct { + GLuint source, palette; + int x, y, depth, cx, cy, valid, pinned; + uint64_t stamp, used, checked_epoch; + int next; +} TexelCacheEntry; +static TexelCacheEntry s_tc[TC_CAP]; +static uint64_t s_tc_tiles[32][64], s_tc_epoch, s_tc_clock; +static uint64_t s_tc_hits, s_tc_misses, s_tc_evictions; +static GLuint s_tc_array, s_tc_fbo, s_tc_prog; +static GLint s_tc_page, s_tc_clut, s_tc_depth; +static int s_tc_ready = -1, s_tc_capacity = TC_CAP; +static TcAttachLayer s_tc_attach; +static int s_tc_pins[TC_CAP], s_tc_pin_n; +static int s_tc_heads[1024], s_tc_last = -1; +static int texel_cache_lookup_fast(void) { + static int enabled = -1; + if (enabled < 0) { + const char *e = getenv("PSX_GL_TEXEL_LOOKUP_FAST"); + enabled = !(e && e[0] == '0'); + } + return enabled; +} +static unsigned texel_cache_hash(GLuint source, GLuint palette, int x, int y, int depth, int cx, int cy) { + uint32_t h = source * 0x9e3779b9u ^ palette * 0x85ebca6bu; + h ^= (uint32_t)x * 73856093u ^ (uint32_t)y * 19349663u; + h ^= (uint32_t)cx * 83492791u ^ (uint32_t)cy * 2654435761u ^ (uint32_t)depth; + h ^= h >> 16; + return h & 1023u; +} +static int texel_cache_matches(const TexelCacheEntry *c, GLuint source, GLuint palette, + int x, int y, int depth, int cx, int cy) { + return c->valid && c->source==source && c->palette==palette && c->x==x && c->y==y && + c->depth==depth && c->cx==cx && c->cy==cy; +} +static void texel_cache_pin(int slot) { + if (!s_tc[slot].pinned) { s_tc[slot].pinned=1; s_tc_pins[s_tc_pin_n++]=slot; } +} +static void texel_cache_unpin(void) { + for (int i=0; i 1023) x1 = 1023; if (y1 > 511) y1 = 511; + if (x1 < x0 || y1 < y0) return; + uint64_t stamp = ++s_tc_epoch; + for (int y=y0/16; y<=y1/16; ++y) + for (int x=x0/16; x<=x1/16; ++x) s_tc_tiles[y][x] = stamp; +} +static uint64_t texel_cache_region(int x, int y, int w, int h) { + uint64_t stamp = 0; + for (int ty=y/16; ty<=(y+h-1)/16; ++ty) + for (int tx=x/16; tx<=(x+w-1)/16; ++tx) { + uint64_t v = s_tc_tiles[ty & 31][tx & 63]; + if (v > stamp) stamp = v; + } + return stamp; +} +static void texel_cache_reset(void) { + memset(s_tc, 0, sizeof s_tc); + memset(s_tc_heads, 0, sizeof s_tc_heads); + s_tc_last = -1; + memset(s_tc_tiles, 0, sizeof s_tc_tiles); + s_tc_epoch = s_tc_clock = s_tc_hits = s_tc_misses = s_tc_evictions = 0; + s_tc_array = s_tc_fbo = s_tc_prog = 0; + s_tc_ready = -1; + s_tc_pin_n = 0; +} +static int texel_cache_init(void) { + if (s_tc_ready >= 0) return s_tc_ready; + s_tc_ready = 0; + const char *e = getenv("PSX_GL_TEXEL_CACHE"); + if (e && e[0] == '0') return 0; + e = getenv("PSX_GL_TEXEL_CACHE_LAYERS"); + s_tc_capacity = e ? atoi(e) : TC_CAP; + if (s_tc_capacity < 4) s_tc_capacity = 4; + if (s_tc_capacity > TC_CAP) s_tc_capacity = TC_CAP; + TcImage3D image = (TcImage3D)SDL_GL_GetProcAddress("glTexImage3D"); + s_tc_attach = (TcAttachLayer)SDL_GL_GetProcAddress("glFramebufferTextureLayer"); + if (!image || !s_tc_attach) return 0; + /* Reuse the literal native address/decode functions from TEX_FS. The + * decoder has no window or UV bounds: fetch_texel applies those before + * it samples the decoded layer. This avoids a second decode definition. */ + const char *helpers = strstr(TEX_FS, "int vram_at("); + const char *fetch = strstr(TEX_FS, "int fetch_texel("); + const char *native = strstr(TEX_FS, " if (v_depth == 0)"); + const char *end = strstr(TEX_FS, "vec3 col5("); + if (!helpers || !fetch || !native || !end) return 0; + char *fs = malloc(strlen(TEX_FS) + 512); + if (!fs) return 0; + snprintf(fs, strlen(TEX_FS) + 512, + "#version 330\nuniform usampler2D u_vram, u_palette;\n" + "uniform ivec2 v_tpage, v_clut; uniform int v_depth;\n" + "layout(location=0) out uint decoded_word;\n%.*s" + "int decode_native(int u,int v){\n%.*s" + "void main(){decoded_word=uint(decode_native(int(gl_FragCoord.x),int(gl_FragCoord.y)));}\n", + (int)(fetch-helpers), helpers, (int)(end-native), native); + s_tc_prog = build_program(PRESENT_VS, fs); + free(fs); + if (!s_tc_prog) return 0; + glGenTextures(1, &s_tc_array); + p_glActiveTexture(PSXGL_TEXTURE0 + 3); + glBindTexture(GL_TEXTURE_2D_ARRAY, s_tc_array); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + p_glBindBuffer(PSXGL_PIXEL_UNPACK_BUFFER, 0); + image(GL_TEXTURE_2D_ARRAY, 0, PSXGL_R16UI, 256, 256, s_tc_capacity, 0, + PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, NULL); + p_glActiveTexture(PSXGL_TEXTURE0); + if (glGetError() != GL_NO_ERROR) return 0; + p_glGenFramebuffers(1, &s_tc_fbo); + s_tc_page = p_glGetUniformLocation(s_tc_prog, "v_tpage"); + s_tc_clut = p_glGetUniformLocation(s_tc_prog, "v_clut"); + s_tc_depth = p_glGetUniformLocation(s_tc_prog, "v_depth"); + p_glUseProgram(s_tc_prog); + p_glUniform1i(p_glGetUniformLocation(s_tc_prog, "u_vram"), 0); + p_glUniform1i(p_glGetUniformLocation(s_tc_prog, "u_palette"), 1); + s_tc_ready = 1; + return 1; +} +static void texel_cache_bind(void) { + p_glActiveTexture(PSXGL_TEXTURE0 + 3); + glBindTexture(GL_TEXTURE_2D_ARRAY, s_tc_array); + p_glActiveTexture(PSXGL_TEXTURE0); +} +static int texel_cache_get(GLuint source, GLuint palette, int x, int y, int depth, int cx, int cy) { + if (depth >= 2 || source != s_raw_tex || palette != s_raw_tex || + !texel_cache_init()) return -1; + const int fast = texel_cache_lookup_fast(); + const unsigned bucket = texel_cache_hash(source,palette,x,y,depth,cx,cy); + if (fast) { + int hit = s_tc_last; + if (hit < 0 || !texel_cache_matches(&s_tc[hit],source,palette,x,y,depth,cx,cy)) { + hit = s_tc_heads[bucket] - 1; + while (hit >= 0 && !texel_cache_matches(&s_tc[hit],source,palette,x,y,depth,cx,cy)) + hit = s_tc[hit].next - 1; + } + if (hit >= 0) { + TexelCacheEntry *c = &s_tc[hit]; + if (c->checked_epoch != s_tc_epoch) { + uint64_t stamp = source == s_raw_tex ? texel_cache_region(x,y,depth ? 128 : 64,256) : 0; + uint64_t ps = palette == s_raw_tex ? texel_cache_region(cx,cy,depth ? 256 : 16,1) : 0; + if (ps > stamp) stamp = ps; + if (c->stamp == stamp) c->checked_epoch = s_tc_epoch; + } + if (c->checked_epoch == s_tc_epoch) { + c->used=++s_tc_clock; texel_cache_pin(hit); s_tc_last=hit; s_tc_hits++; return hit; + } + } + } + uint64_t stamp = source == s_raw_tex ? texel_cache_region(x,y,depth ? 128 : 64,256) : 0; + uint64_t ps = palette == s_raw_tex ? texel_cache_region(cx,cy,depth ? 256 : 16,1) : 0; + if (ps > stamp) stamp = ps; + int slot = -1; + uint64_t oldest = UINT64_MAX; + for (int i=0; istamp == stamp) { c->used=++s_tc_clock; texel_cache_pin(i); s_tc_hits++; return i; } + } + if (!c->valid) { if (oldest != 0) { slot=i; oldest=0; } } + else if (!c->pinned && c->used < oldest) { oldest=c->used; slot=i; } + } + s_tc_misses++; + if (slot < 0) return -1; /* All layers pinned: preserve the original batch. */ + if (s_tc[slot].valid) { + /* Only layers without deferred CPU draw references can be reused. + * GPU readers were already submitted, so command ordering protects + * them. Never split a mask-checked batch to make cache space. */ + s_tc_evictions++; + + } + p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_tc_fbo); + s_tc_attach(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, s_tc_array, 0, slot); + if (p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER) != PSXGL_FRAMEBUFFER_COMPLETE) return -1; + glViewport(0,0,256,256); + glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_STENCIL_TEST); + glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE); + p_glUseProgram(s_tc_prog); + p_glUniform2i(s_tc_page,x,y); p_glUniform2i(s_tc_clut,cx,cy); p_glUniform1i(s_tc_depth,depth); + p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D,source); + p_glActiveTexture(PSXGL_TEXTURE0+1); glBindTexture(GL_TEXTURE_2D,palette); + p_glActiveTexture(PSXGL_TEXTURE0+3); glBindTexture(GL_TEXTURE_2D_ARRAY,0); + p_glBindVertexArray(s_empty_vao); glDrawArrays(GL_TRIANGLES,0,3); + p_glBindFramebuffer(PSXGL_FRAMEBUFFER,s_hr_fbo); + p_glActiveTexture(PSXGL_TEXTURE0); + if (s_tc[slot].valid) { + TexelCacheEntry *c = &s_tc[slot]; + unsigned old = texel_cache_hash(c->source,c->palette,c->x,c->y,c->depth,c->cx,c->cy); + int *link = &s_tc_heads[old]; + while (*link && *link != slot+1) link = &s_tc[*link-1].next; + if (*link) *link = c->next; + } + s_tc[slot]=(TexelCacheEntry){.source=source,.palette=palette,.x=x,.y=y,.depth=depth, + .cx=cx,.cy=cy,.valid=1,.stamp=stamp,.used=++s_tc_clock, + .checked_epoch=s_tc_epoch,.next=s_tc_heads[bucket]}; + s_tc_heads[bucket]=slot+1; + s_tc_last=slot; + texel_cache_pin(slot); + return slot; +} diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 4a38252e..0cfb6b7a 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -799,6 +799,10 @@ static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0; static int s_hr_alloc = 0; /* Native raw-1555 sampling mirror + readback source. */ static GLuint s_raw_tex = 0, s_raw_fbo = 0; +static void texel_cache_dirty(int x0, int y0, int x1, int y1); +static void texel_cache_reset(void); +static void texel_cache_bind(void); +static void texel_cache_unpin(void); /* CPU->VRAM upload staging (native RGBA8). */ static GLuint s_up_tex = 0; /* copy_rect staging (hr-sized RGBA8). */ @@ -1423,6 +1427,8 @@ static void present_target_quad(GLuint tex, float tex_w, float tex_h, int apply_gamma, int src_scale); static void coh_record(int kind, int x0, int y0, int x1, int y1) { + if (kind == GL_COH_PACK || kind == GL_COH_FLUSH || kind == GL_COH_UPLOAD) + texel_cache_dirty(x0,y0,x1,y1); GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP]; e->frame = (uint32_t)s_frame_count; e->kind = (uint8_t)kind; @@ -2002,6 +2008,7 @@ static const char *TEX_FS = "uniform sampler2D u_hd_texture;\n" "uniform usampler2D u_vram;\n" "uniform usampler2D u_palette;\n" + "uniform usampler2DArray u_decoded;\n" "uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n" "uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n" "uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n" @@ -2029,6 +2036,7 @@ static const char *TEX_FS = " u = clamp(u, v_limits.x, v_limits.z);\n" " v = clamp(v, v_limits.y, v_limits.w);\n" " }\n" + " if (v_depth >= 4) return int(texelFetch(u_decoded,ivec3(u,v,v_depth-4),0).r);\n" " if (v_depth == 0) {\n" " int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n" " return palette_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n" @@ -3675,7 +3683,9 @@ static void bind_textured_resources(GLuint source, GLuint palette, GLuint hd) { p_glActiveTexture(PSXGL_TEXTURE0 + 2); glBindTexture(GL_TEXTURE_2D, hd ? hd : s_up_tex); p_glUniform1i(s_uHdTexture, 2); p_glActiveTexture(PSXGL_TEXTURE0); + texel_cache_bind(); } +#include "gl_texel_cache.inc" static int s_tb_twin[4] = {0, 0, 0, 0}; static uint64_t s_batch_total = 0, s_batch_reason[8]; @@ -4211,6 +4221,7 @@ static void hiw_flush_queue(void) { } } hiw_flush_tail(); + if (!s_tb_n && !s_hq_n) texel_cache_unpin(); } /* The native-wide mirrors (both modes), then the canonical projection back @@ -4289,6 +4300,7 @@ static void flush_tex_batch_impl(void) { } hr_end(); if (--s_cw_flush_depth == 0) s_cw_flush_ms += cw_ms() - cw_t0; + if (!s_tb_n && !s_hq_n) texel_cache_unpin(); } /* Flat / gouraud GEO batch — MotK title/char-select starfields issue ~30k/s @@ -4980,6 +4992,13 @@ static void gpu_textured_triangle(const int *xs, const int *ys, flush_tex_batch(); } if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); } + const int cache_bounds = (twx | twy) || + (lim[0] >= 0 && lim[1] >= 0 && lim[2] <= 255 && lim[3] <= 255 && + lim[0] <= lim[2] && lim[1] <= lim[3]); + int decoded = hd_tex || !cache_bounds ? -1 : texel_cache_get( + s_selected_bank_tex ? s_selected_bank_tex : s_raw_tex, + s_selected_bank_tex && !s_selected_bank_live_clut ? s_selected_bank_tex : s_raw_tex, + base_x,base_y,depth,clut_x,clut_y); if (s_tb_n == 0) { /* opening a batch: capture its keyed state */ s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = filter; s_tb_gate = gate; s_tb_depth = tdmode; @@ -5000,7 +5019,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys, vp[4] = col[i*3+0]; vp[5] = col[i*3+1]; vp[6] = col[i*3+2]; vp[7] = 1.0f; vp[8] = (float)base_x; vp[9] = (float)base_y; /* a_tpage */ vp[10] = (float)clut_x; vp[11] = (float)clut_y; /* a_clut */ - vp[12] = (float)depth; vp[13] = (float)rawtex; /* a_depth, a_raw */ + vp[12] = (float)(decoded < 0 ? depth : decoded + 4); + vp[13] = (float)rawtex; /* a_depth, a_raw */ vp[14] = (float)lim[0]; vp[15] = (float)lim[1]; /* a_limits */ vp[16] = (float)lim[2]; vp[17] = (float)lim[3]; vp[18] = (semi >= 0 ? (float)(semi + 1) : 0.0f) /* a_semi code */ @@ -5725,6 +5745,7 @@ static void depth24_clear_skipped_fb(void) { glViewport(0, 0, VRAM_W, VRAM_H); glScissor(x0, y0, rw, rh); glClear(GL_COLOR_BUFFER_BIT); + texel_cache_dirty(x0,y0,x1,y1); } glDisable(GL_SCISSOR_TEST); p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); @@ -6221,6 +6242,7 @@ static int init_gpu_raster(void) { s_tex_uBcls = p_glGetUniformLocation(s_tex_prog, "u_bcls"); p_glUseProgram(s_tex_prog); p_glUniform1i(p_glGetUniformLocation(s_tex_prog, "u_coverage_fast"), coverage_fast_enabled()); + p_glUniform1i(p_glGetUniformLocation(s_tex_prog, "u_decoded"), 3); s_geo_uBcls = p_glGetUniformLocation(s_geo_prog, "u_bcls"); s_geo_uBadd = p_glGetUniformLocation(s_geo_prog, "u_badd"); s_uBlitSrc = p_glGetUniformLocation(s_blit_prog, "u_src"); @@ -6542,6 +6564,7 @@ int gl_renderer_select_texture_bank_live_clut(uint16_t id) { int gl_renderer_init_context(SDL_Window *win) { s_win = win; + texel_cache_reset(); s_window_owner = window_thread_id(); #if defined(PSX_SDL3) if (s_drawable_watch) SDL_RemoveEventWatch(drawable_event_watch, NULL); @@ -8631,6 +8654,7 @@ static void pass_journal_rollback(void) { pass_blit(j->hr_fbo, s_hr_fbo, 0, 0, x * S, y * S, w * S, h * S, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); pass_blit(j->raw_fbo, s_raw_fbo, 0, 0, x, y, w, h, GL_COLOR_BUFFER_BIT); + texel_cache_dirty(x,y,x+w-1,y+h-1); } render_pass_journal_rollback(&s_pj_cpu, s_vram, VRAM_W); } @@ -8807,6 +8831,7 @@ static void transaction_restore_ex(int keep_color) { s_pass_w * S, s_pass_h * S, hr_bits); pass_blit(s_pb_raw_fbo, s_raw_fbo, 0, 0, s_pass_x, s_pass_y, s_pass_w, s_pass_h, GL_COLOR_BUFFER_BIT); + texel_cache_dirty(s_pass_x,s_pass_y,s_pass_x+s_pass_w-1,s_pass_y+s_pass_h-1); if (s_pb_wide_src && s_pb_wide_src == pass_wide_fbo_for(s_pass_x)) { pass_blit(s_pb_wide_fbo, s_pb_wide_src, 0, 0, 0, s_pass_y * S, g_wide_w * S, s_pass_h * S, hr_bits); diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 91d3faac..566c586c 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1368,6 +1368,7 @@ extern "C" int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc) { const bool usable = s_nclip_exact_enabled && s_nclip_last_precise_valid && native_mac0 == s_nclip_last_native; const int32_t sign = usable ? s_nclip_last_precise_sign : native; +#ifndef PSX_NO_DEBUG_TOOLS if (!s_gte_replay_sandbox && s_speculative_depth == 0) { if (GteNclipSiteStat *e = nclip_find(s_nclip_site, pc, &GteNclipSiteStat::pc, @@ -1378,6 +1379,9 @@ extern "C" int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc) { e->last_frame = (uint32_t)s_frame_count; } } +#else + (void)pc; +#endif return sign; } extern "C" int gte_nclip_native_wide_sign(int32_t native_mac0, int* sign) { @@ -1487,6 +1491,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { const bool disagree = (cross > 0 && out <= 0) || (cross < 0 && out >= 0); if (disagree) s_nclip_disagreements++; +#ifndef PSX_NO_DEBUG_TOOLS /* Last guest store before this NCLIP: codegen stamps it on every * store, so it lands inside the emitter that issued the NCLIP (the * function stamp is not maintained for static code). */ @@ -1502,6 +1507,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) { e->saturated++; e->last_frame = (uint32_t)s_frame_count; } +#endif } else if (gpu_ws_precise_nclip_enabled() && !s_gte_replay_sandbox && s_speculative_depth == 0) { s_nclip_fallbacks++;