Skip to content

Commit d9282e9

Browse files
GWToolbox Botclaude
andcommitted
perf(skill-range-rings): cache draped geometry, re-drape only when anchor moves
The rings were re-draped onto the terrain every frame, and each vertex samples the surface via GW::Map::QueryAltitude across every pathing plane. A large ring is up to ~1000 vertices * n_planes altitude queries per frame, collapsing FPS into single digits (#2408) even while standing still and just hovering a skill. Cache the built vertex buffer and only rebuild it when an anchor (player or target) actually moves, the hovered skill changes, or settings change. Stationary hovering now costs a single drape instead of one per frame. Refs #2408 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 25bea3f commit d9282e9

1 file changed

Lines changed: 35 additions & 11 deletions

File tree

GWToolboxdll/Modules/SkillRangeRingsModule.cpp

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,23 @@ namespace {
5151
bool at_target;
5252
};
5353

54-
// Rings are rebuilt into `scratch` (absolute world coords) every frame and drawn with DrawPrimitiveUP,
55-
// so following the character is smooth. The per-ring specs only change when the hovered skill or the
56-
// settings change.
54+
// Draped ring geometry (absolute world coords) drawn with DrawPrimitiveUP. The per-vertex terrain drape
55+
// is expensive, so `scratch` is cached and only rebuilt when an anchor moves, the hovered skill changes,
56+
// or settings change - not every frame.
5757
std::vector<RingSpec> built_specs;
5858
std::vector<RingVertex> scratch;
5959
GW::Constants::SkillID built_skill = static_cast<GW::Constants::SkillID>(0);
6060
bool rings_dirty = false;
6161
uint32_t debug_skill_id = 0;
6262
int compositor_token = 0;
6363

64+
// Anchor state `scratch` was last draped at; a change beyond kAnchorMoveEpsilon triggers a rebuild.
65+
constexpr float kAnchorMoveEpsilon = 1.f;
66+
float built_me_x = 0.f, built_me_y = 0.f;
67+
uint32_t built_me_zplane = 0, built_target_id = 0, built_target_zplane = 0;
68+
float built_target_x = 0.f, built_target_y = 0.f;
69+
bool built_tgt_valid = false;
70+
6471
int RingSegments(const float radius)
6572
{
6673
const auto circumference = std::max(radius, ring_thickness) * DirectX::XM_2PI;
@@ -147,11 +154,13 @@ namespace {
147154
SpecsForSkill(skill, built_specs);
148155
built_skill = skill.skill_id;
149156
rings_dirty = false;
157+
scratch.clear();
150158
}
151159

152160
void ResetRings()
153161
{
154162
built_specs.clear();
163+
scratch.clear();
155164
built_skill = static_cast<GW::Constants::SkillID>(0);
156165
}
157166
} // namespace
@@ -199,16 +208,31 @@ void SkillRangeRingsModule::DrawInWorld(IDirect3DDevice9* device)
199208
if (rings_dirty || skill->skill_id != built_skill) BuildSpecs(*skill);
200209
if (built_specs.empty()) return;
201210

202-
// Rebuild the draped rings at the current anchor positions every frame, so movement is smooth. Each
203-
// ring anchors to the current target (targeted AoE skills) or the player, draped onto that surface.
211+
// Re-drape only when an anchor actually moves; otherwise reuse the cached geometry. Each ring anchors to
212+
// the current target (targeted AoE skills) or the player, draped onto that surface.
204213
const GW::Agent* target = GW::Agents::GetTarget();
205214
const bool tgt_valid = aoe_at_target && target && target->agent_id != me->agent_id;
206-
const auto n_planes = TerrainDrape::PathingPlaneCount();
207-
scratch.clear();
208-
for (const auto& spec : built_specs) {
209-
const GW::Agent* anchor = (spec.at_target && tgt_valid) ? target : static_cast<const GW::Agent*>(me);
210-
const float ref_z = TerrainDrape::SurfaceZ(anchor->pos.x, anchor->pos.y, anchor->pos.zplane, n_planes);
211-
EmitBand(scratch, anchor->pos.x, anchor->pos.y, anchor->pos.zplane, n_planes, ref_z ? ref_z : anchor->z, spec);
215+
const uint32_t target_id = tgt_valid ? target->agent_id : 0;
216+
const auto moved = [](const float ax, const float ay, const float bx, const float by) {
217+
return std::fabs(ax - bx) > kAnchorMoveEpsilon || std::fabs(ay - by) > kAnchorMoveEpsilon;
218+
};
219+
const bool rebuild = scratch.empty()
220+
|| tgt_valid != built_tgt_valid || target_id != built_target_id
221+
|| me->pos.zplane != built_me_zplane || moved(me->pos.x, me->pos.y, built_me_x, built_me_y)
222+
|| (tgt_valid && (target->pos.zplane != built_target_zplane
223+
|| moved(target->pos.x, target->pos.y, built_target_x, built_target_y)));
224+
if (rebuild) {
225+
const auto n_planes = TerrainDrape::PathingPlaneCount();
226+
scratch.clear();
227+
for (const auto& spec : built_specs) {
228+
const GW::Agent* anchor = (spec.at_target && tgt_valid) ? target : static_cast<const GW::Agent*>(me);
229+
const float ref_z = TerrainDrape::SurfaceZ(anchor->pos.x, anchor->pos.y, anchor->pos.zplane, n_planes);
230+
EmitBand(scratch, anchor->pos.x, anchor->pos.y, anchor->pos.zplane, n_planes, ref_z ? ref_z : anchor->z, spec);
231+
}
232+
built_me_x = me->pos.x, built_me_y = me->pos.y, built_me_zplane = me->pos.zplane;
233+
built_target_id = target_id, built_tgt_valid = tgt_valid;
234+
built_target_x = tgt_valid ? target->pos.x : 0.f, built_target_y = tgt_valid ? target->pos.y : 0.f;
235+
built_target_zplane = tgt_valid ? target->pos.zplane : 0;
212236
}
213237
if (scratch.size() < 3) return;
214238

0 commit comments

Comments
 (0)