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:
@@ -450,6 +450,20 @@ class SceneCache:
|
||||
lods.append((sw_in, sw_out, lg))
|
||||
if not lods:
|
||||
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
|
||||
# the same dcs_link rig as the camera (torso 0x1f-animated) --
|
||||
# nest-only chains left the player mech rendering at world origin.
|
||||
|
||||
@@ -303,4 +303,5 @@ while True:
|
||||
last_report = time.time()
|
||||
print(f"frames={frames} nodes={len(board.nodes)} "
|
||||
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)
|
||||
|
||||
@@ -581,13 +581,25 @@ static Bitu vpx_read(Bitu port, Bitu /*iolen*/) {
|
||||
no_pick = getenv("VPX_NO_PICK") ? 1 : 0;
|
||||
unsigned pinst = 0, pdcs = 0, pgg = 0, pgeom = 0;
|
||||
float pxyz[3] = { 0, 0, 0 };
|
||||
if (!no_pick && sect_armed &&
|
||||
raycast_pick(&pinst, &pdcs, &pgg, &pgeom, pxyz)) {
|
||||
queue_sect_frame_reply(pinst, pdcs, pgg, pgeom, pxyz);
|
||||
if (!no_pick && sect_armed) {
|
||||
/* ALWAYS answer the armed pick, hit or
|
||||
* 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();
|
||||
fprintf(vpx_fp, "# post-boot: frame ack "
|
||||
"(9) + sect inst=%08X dcs=%08X\n",
|
||||
pinst, pdcs); fflush(vpx_fp); }
|
||||
"(9) + sect %s inst=%08X\n",
|
||||
hit ? "hit" : "MISS", pinst);
|
||||
fflush(vpx_fp); }
|
||||
} else {
|
||||
queue_render_ack(9);
|
||||
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.
|
||||
* Traversal mirrors scene_publish_frame: dcs -> instance -> object -> lod[0] ->
|
||||
* 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,
|
||||
unsigned *geom_out, float xyz_out[3]) {
|
||||
/* 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];
|
||||
const bool from_bridge = bridge_cam_get(O, D);
|
||||
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];
|
||||
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]);
|
||||
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;
|
||||
/* 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:
|
||||
@@ -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 =
|
||||
S.children.find(oi->second);
|
||||
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 =
|
||||
S.children.find(li->second[0]); /* lod[0] */
|
||||
S.children.find(li->second[ldx]);
|
||||
if (ggi == S.children.end()) continue;
|
||||
for (size_t g = 0; g < ggi->second.size(); g++) {
|
||||
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[1] = O[1] + D[1] * best_t;
|
||||
xyz_out[2] = O[2] + D[2] * best_t;
|
||||
|
||||
Reference in New Issue
Block a user