Aim pipeline: all-LODs raycast, always-answer picks, own-mech exclusion

The stale-aim saga, root-caused live with the operator:

- The device raycast walked only lod[0] of each object; BT content is
  authored ADDITIVE_LODS (arena wall = posts lod + panel lod), so the
  aim ray flew BETWEEN the skeleton pieces of whatever was targeted --
  94% miss rate (pick stats now log this).
- Misses fell back to a plain frame ack carrying no sect data, so the
  game kept a seconds-old target: "aim updates every few seconds",
  stationary shots stale. The pick is now answered EVERY armed frame;
  a miss is a valid all-zero reply that clears targetEntity (aiming at
  open sky un-targets -- authentic misfire, not stale fire).
- Raycast walks ALL lod children; own-articulation exclusion via the
  CAM backchannel 7th field (vehicle root DCS) stops mid-volley picks
  of the player's own muzzle flash / arms.

Renderer: same ADDITIVE_LODS fix -- when every LOD switch window is
the degenerate (0,1e9) fallback, draw the union of all lods. The
arena wall ring (user-confirmed live) went from floating post slivers
to the full textured wall. Bridge reports its wire backlog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-08 14:33:50 -05:00
co-authored by Claude Fable 5
parent 351cce26b3
commit d88d9212c0
3 changed files with 66 additions and 10 deletions
+14
View File
@@ -450,6 +450,20 @@ class SceneCache:
lods.append((sw_in, sw_out, lg)) lods.append((sw_in, sw_out, lg))
if not lods: if not lods:
continue continue
# BT's wire LOD bodies carry no usable switch distances (zeros ->
# the (0,1e9) fallback on every lod), and much of its content is
# authored ADDITIVE_LODS (BGF SV_SPECIAL: the arena walls are a
# posts-lod + panel-lod that draw TOGETHER). Range-select over
# identical windows always took lods[0] -- the wall posts without
# the wall. When every window is the degenerate fallback, draw
# the union of all lods; genuine distance windows keep switching.
if len(lods) > 1 and all(a == 0.0 and b >= 1e8 for a, b, _ in lods):
allg = []
for _, _, lg in lods:
for g in lg:
if g not in allg:
allg.append(g)
lods = [(0.0, 1e9, allg)]
# MUNGA: vehicle/mech part instances get their world pose through # MUNGA: vehicle/mech part instances get their world pose through
# the same dcs_link rig as the camera (torso 0x1f-animated) -- # the same dcs_link rig as the camera (torso 0x1f-animated) --
# nest-only chains left the player mech rendering at world origin. # nest-only chains left the player mech rendering at world origin.
+2 -1
View File
@@ -303,4 +303,5 @@ while True:
last_report = time.time() last_report = time.time()
print(f"frames={frames} nodes={len(board.nodes)} " print(f"frames={frames} nodes={len(board.nodes)} "
f"uploads={len(board.uploads)} tex={len(board.tex)} " f"uploads={len(board.uploads)} tex={len(board.tex)} "
f"munga={board.munga} anim_abs={len(board.anim_abs)}", flush=True) f"munga={board.munga} anim_abs={len(board.anim_abs)} "
f"backlog={len(pending)}B", flush=True)
+50 -9
View File
@@ -581,13 +581,25 @@ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
no_pick = getenv("VPX_NO_PICK") ? 1 : 0; no_pick = getenv("VPX_NO_PICK") ? 1 : 0;
unsigned pinst = 0, pdcs = 0, pgg = 0, pgeom = 0; unsigned pinst = 0, pdcs = 0, pgg = 0, pgeom = 0;
float pxyz[3] = { 0, 0, 0 }; float pxyz[3] = { 0, 0, 0 };
if (!no_pick && sect_armed && if (!no_pick && sect_armed) {
raycast_pick(&pinst, &pdcs, &pgg, &pgeom, pxyz)) { /* ALWAYS answer the armed pick, hit or
queue_sect_frame_reply(pinst, pdcs, pgg, pgeom, pxyz); * miss. A miss = all-zero reply: the game
* clears targetEntity (L4VIDEO: no hit ->
* NULL) so aiming at open sky un-targets
* instead of freezing the LAST hit -- the
* old hit-only path made the aim go STALE
* whenever the ray cleared the arena
* walls (plain acks carry no sect data,
* so the game kept seconds-old targets). */
bool hit = raycast_pick(&pinst, &pdcs,
&pgg, &pgeom, pxyz);
queue_sect_frame_reply(pinst, pdcs, pgg,
pgeom, pxyz);
if (vpx_fp) { flush_run(); if (vpx_fp) { flush_run();
fprintf(vpx_fp, "# post-boot: frame ack " fprintf(vpx_fp, "# post-boot: frame ack "
"(9) + sect inst=%08X dcs=%08X\n", "(9) + sect %s inst=%08X\n",
pinst, pdcs); fflush(vpx_fp); } hit ? "hit" : "MISS", pinst);
fflush(vpx_fp); }
} else { } else {
queue_render_ack(9); queue_render_ack(9);
if (vpx_fp) { flush_run(); if (vpx_fp) { flush_run();
@@ -2201,6 +2213,27 @@ static float ray_tri(const float O[3], const float D[3],
* and the world hit point. Same picking Dave's renderer does, self-contained. * and the world hit point. Same picking Dave's renderer does, self-contained.
* Traversal mirrors scene_publish_frame: dcs -> instance -> object -> lod[0] -> * Traversal mirrors scene_publish_frame: dcs -> instance -> object -> lod[0] ->
* geogroup -> geometry -> polys, transformed by the DCS world matrix. */ * geogroup -> geometry -> polys, transformed by the DCS world matrix. */
/* Pick outcome stats, printed to the VPXLOG every 256 calls: the aim only
* refreshes game-side when a sect reply lands, so a high miss rate IS the
* "stale aim" symptom. fail reasons: 1=no camera, 2=degenerate dir, 3=ray
* hit nothing (aim above the walls / into open sky). */
static unsigned pick_calls = 0, pick_oks = 0, pick_bridge = 0;
static unsigned pick_fails[4];
static void pick_stat_print(void) {
if (++pick_calls % 256 || !vpx_fp) return;
fprintf(vpx_fp, "# pick stats: calls=%u ok=%u (bridge=%u) "
"fail nocam=%u dir=%u nohit=%u\n",
pick_calls, pick_oks, pick_bridge,
pick_fails[1], pick_fails[2], pick_fails[3]);
fflush(vpx_fp);
}
static void pick_stat_fail(int r) { pick_fails[r & 3]++; pick_stat_print(); }
static void pick_stat_ok(bool bridge) {
pick_oks++; if (bridge) pick_bridge++;
pick_stat_print();
}
static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out, static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out,
unsigned *geom_out, float xyz_out[3]) { unsigned *geom_out, float xyz_out[3]) {
/* Ray = the player's actual look: prefer the bridge's camera (cam chain /* Ray = the player's actual look: prefer the bridge's camera (cam chain
@@ -2211,12 +2244,12 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out
float O[3], D[3]; float O[3], D[3];
const bool from_bridge = bridge_cam_get(O, D); const bool from_bridge = bridge_cam_get(O, D);
if (!from_bridge) { if (!from_bridge) {
if (!S.view.has_cam) return false; if (!S.view.has_cam) { pick_stat_fail(1); return false; }
O[0] = S.view.eye[0]; O[1] = S.view.eye[1]; O[2] = S.view.eye[2]; O[0] = S.view.eye[0]; O[1] = S.view.eye[1]; O[2] = S.view.eye[2];
D[0] = -S.view.rot[6]; D[1] = -S.view.rot[7]; D[2] = -S.view.rot[8]; D[0] = -S.view.rot[6]; D[1] = -S.view.rot[7]; D[2] = -S.view.rot[8];
} }
float dl = sqrtf(D[0]*D[0] + D[1]*D[1] + D[2]*D[2]); float dl = sqrtf(D[0]*D[0] + D[1]*D[1] + D[2]*D[2]);
if (dl < 1e-6f) return false; if (dl < 1e-6f) { pick_stat_fail(2); return false; }
D[0] /= dl; D[1] /= dl; D[2] /= dl; D[0] /= dl; D[1] /= dl; D[2] /= dl;
/* Report the TRUE nearest hit -- terrain IS a valid target (you must be /* Report the TRUE nearest hit -- terrain IS a valid target (you must be
* able to fire into the ground and miss). Return ALL of the hit handles: * able to fire into the ground and miss). Return ALL of the hit handles:
@@ -2269,8 +2302,14 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out
std::map<unsigned, std::vector<unsigned> >::const_iterator li = std::map<unsigned, std::vector<unsigned> >::const_iterator li =
S.children.find(oi->second); S.children.find(oi->second);
if (li == S.children.end() || li->second.empty()) continue; if (li == S.children.end() || li->second.empty()) continue;
/* Walk ALL lod children, not just lod[0]: BT content is authored
* ADDITIVE_LODS (the arena wall = a posts lod + the actual wall
* panel in ANOTHER lod). Picking lod[0] only made the ray pass
* BETWEEN the posts of whatever the player aimed at -- 94% miss
* rate, stale/cleared targets, boo-beep on every trigger. */
for (size_t ldx = 0; ldx < li->second.size(); ldx++) {
std::map<unsigned, std::vector<unsigned> >::const_iterator ggi = std::map<unsigned, std::vector<unsigned> >::const_iterator ggi =
S.children.find(li->second[0]); /* lod[0] */ S.children.find(li->second[ldx]);
if (ggi == S.children.end()) continue; if (ggi == S.children.end()) continue;
for (size_t g = 0; g < ggi->second.size(); g++) { for (size_t g = 0; g < ggi->second.size(); g++) {
unsigned gg = ggi->second[g]; /* geogroup handle */ unsigned gg = ggi->second[g]; /* geogroup handle */
@@ -2308,9 +2347,11 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out
} }
} }
} }
} /* all-lods walk */
} }
} }
if (!best_inst) return false; if (!best_inst) { pick_stat_fail(3); return false; }
pick_stat_ok(from_bridge);
xyz_out[0] = O[0] + D[0] * best_t; xyz_out[0] = O[0] + D[0] * best_t;
xyz_out[1] = O[1] + D[1] * best_t; xyz_out[1] = O[1] + D[1] * best_t;
xyz_out[2] = O[2] + D[2] * best_t; xyz_out[2] = O[2] + D[2] * best_t;