From 6bfcac7d685f2674fca289ae4bd8d775fca5686e Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 30 Sep 2026 11:39:01 +0900 Subject: [PATCH] RecordingDevice: lend cached geometry instead of copying it per draw A geometry-cache hit moved every vertex vector out of the cache into the draw (deep copy of positions/normals/uv/diffuse/indices/bones each frame). The cache now lends its vectors to the frame's first draw of that geometry and takes them back in Render3DBeginFrame; later draws of the same key+revision in the frame (terrain splat layers, shadow-map pass) are marked shared_geometry and carry no vectors -- the native host looks the GPU geometry up by key+revision, and draw captures expand them (Render3DExpandSharedGeometry). Index reads reuse a scratch buffer. Desktop live game: RenderGame rnd 1.7 -> 1.2 ms, no visual change. Co-Authored-By: Claude Opus 5.5 --- src/host/live_client.cpp | 4 +- src/host/render_state.cpp | 2 +- src/host/vk_frame.cpp | 19 +++- src/platform/EterLib/RecordingDevice.cpp | 123 +++++++++++++++++++---- src/platform/EterLib/RenderCommands3D.h | 9 ++ 5 files changed, 128 insertions(+), 29 deletions(-) diff --git a/src/host/live_client.cpp b/src/host/live_client.cpp index c727edc4..3781cf62 100644 --- a/src/host/live_client.cpp +++ b/src/host/live_client.cpp @@ -362,7 +362,9 @@ int run_live_client( renderer.touch_controller.update_screen_size(int(ui_w), int(ui_h)); renderer.touch_controller.append_ui_commands(cap_ui); } - native_draw_capture::write(capture_out, Render3DDraws(), cap_textures, ui_w, ui_h, cap_ui); + std::vector cap_draws = Render3DDraws(); + Render3DExpandSharedGeometry(cap_draws); + native_draw_capture::write(capture_out, cap_draws, cap_textures, ui_w, ui_h, cap_ui); } if (!screenshot_out.empty() && frames > 0) { diff --git a/src/host/render_state.cpp b/src/host/render_state.cpp index 8242c827..6095911b 100644 --- a/src/host/render_state.cpp +++ b/src/host/render_state.cpp @@ -66,7 +66,7 @@ FixedFunctionState make_fixed_function_state(const Render3DDraw& draw) { const std::uint32_t fog_mode = draw.fog_enable ? (draw.fog_table_mode ? draw.fog_table_mode : (draw.pretransformed ? 4u : draw.fog_vertex_mode)) : 0; state.flags = {1u, !draw.texture0.empty() ? 1u : 0u, !draw.texture1.empty() ? 1u : 0u, fog_mode}; - state.lighting_flags = {draw.lighting, draw.color_vertex, draw.diffuse.empty() ? 0u : 1u, + state.lighting_flags = {draw.lighting, draw.color_vertex, (draw.shared_geometry ? draw.shared_diffuse : !draw.diffuse.empty()) ? 1u : 0u, draw.normalize_normals}; state.material_sources = {draw.diffuse_material_source, draw.ambient_material_source, draw.emissive_material_source, draw.local_viewer}; diff --git a/src/host/vk_frame.cpp b/src/host/vk_frame.cpp index 3f747588..aeb84b2f 100644 --- a/src/host/vk_frame.cpp +++ b/src/host/vk_frame.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace mt_host { @@ -191,14 +192,22 @@ void VulkanWindow::prepare_draws(const std::vector& draws, const T back_buffer_list(in_scene).push_back(clear); continue; } - if (draw.positions.empty() || draw.indices.empty()) continue; - const auto signature = draw.geometry_key ? draw.geometry_revision : geometry_hash(draw); - const GeometryId id{draw.geometry_key, signature}; - auto [it, inserted] = geometries_.try_emplace(id); + decltype(geometries_)::iterator it; + bool inserted = false; + if (draw.shared_geometry) { + // Repeats an earlier draw of this frame, which uploaded (or kept) the geometry. + it = geometries_.find(GeometryId{draw.geometry_key, draw.geometry_revision}); + if (it == geometries_.end()) continue; + } else { + if (draw.positions.empty() || draw.indices.empty()) continue; + const auto signature = draw.geometry_key ? draw.geometry_revision : geometry_hash(draw); + std::tie(it, inserted) = geometries_.try_emplace(GeometryId{draw.geometry_key, signature}); + } auto& geometry = it->second; if (inserted) { upload_geometry(draw, geometry); - } else if (geometry.vertex_count != draw.positions.size() / 3 || geometry.index_count != draw.indices.size()) { + } else if (!draw.shared_geometry && (geometry.vertex_count != draw.positions.size() / 3 || + geometry.index_count != draw.indices.size())) { throw std::runtime_error("geometry key/revision reused with a different size"); } geometry.last_used_frame = frame_number_; diff --git a/src/platform/EterLib/RecordingDevice.cpp b/src/platform/EterLib/RecordingDevice.cpp index 2a668e4f..7d02caab 100644 --- a/src/platform/EterLib/RecordingDevice.cpp +++ b/src/platform/EterLib/RecordingDevice.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -15,6 +16,8 @@ namespace { std::mutex g_draws_mutex; std::vector g_draws; +class RecordingDevice; +RecordingDevice* g_recording_device = nullptr; // reclaims lent geometry in Render3DBeginFrame bool g_in_scene = false; int g_native_terrain_override = -1; bool g_gpu_render_targets = false; @@ -266,9 +269,12 @@ public: m_stageStates[0][D3DTSS_ALPHAARG1] = D3DTA_TEXTURE; m_stageStates[1][D3DTSS_COLOROP] = D3DTOP_DISABLE; m_stageStates[1][D3DTSS_ALPHAOP] = D3DTOP_DISABLE; + g_recording_device = this; } ~RecordingDevice() override { + if (g_recording_device == this) + g_recording_device = nullptr; m_renderTarget->Release(); m_depthStencil->Release(); m_backBuffer->Release(); @@ -530,10 +536,10 @@ public: if (!m_stream) return E_FAIL; const UINT count = index_count(type, primitiveCount); - std::vector indices(count); + m_indexScratch.resize(count); for (UINT i = 0; i < count; ++i) - indices[i] = startVertex + i; - record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices, + m_indexScratch[i] = startVertex + i; + record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, m_indexScratch, m_stream, nullptr, startVertex); return S_OK; } @@ -541,11 +547,10 @@ public: { if (!m_stream || !m_indices) return E_FAIL; - std::vector indices; if (!read_indices(m_indices->bytes.data(), m_indices->bytes.size(), m_indices->format, startIndex, - index_count(type, primitiveCount), m_baseVertex, &indices)) + index_count(type, primitiveCount), m_baseVertex, &m_indexScratch)) return E_FAIL; - record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, indices, + record(type, primitiveCount, m_stream->bytes.data(), m_stream->bytes.size(), m_streamStride, m_indexScratch, m_stream, m_indices, startIndex); return S_OK; } @@ -904,24 +909,32 @@ private: } const std::uint64_t frame_id = UIRenderFrameId(); auto cached = draw.geometry_key ? m_geometry_cache.find(draw.geometry_key) : m_geometry_cache.end(); - const bool reuse = cached != m_geometry_cache.end() && !cached->second.changing && + bool reuse = cached != m_geometry_cache.end() && !cached->second.changing && cached->second.revision == draw.geometry_revision; if (reuse) { - cached->second.last_used = frame_id; - const Render3DDraw& prior = cached->second.geometry; - draw.positions = prior.positions; - draw.rhw = prior.rhw; - draw.vertex_fog = prior.vertex_fog; - draw.normals = prior.normals; - draw.uv0 = prior.uv0; - draw.uv1 = prior.uv1; - draw.diffuse = prior.diffuse; - draw.indices = prior.indices; - draw.bone_indices = prior.bone_indices; - draw.bone_weights = prior.bone_weights; + // The cached vectors are lent to the frame's first draw of this geometry (no copy) and + // taken back by reclaim() when the next frame begins; later draws of the same geometry in + // this frame copy from that draw. A lent draw that never reached g_draws (a dropped shadow + // draw) loses them: the vertices are extracted again below and reclaimed from that draw. + GeometryCacheEntry& entry = cached->second; + entry.last_used = frame_id; + if (!entry.geometry.positions.empty()) + { + move_geometry(entry.geometry, draw); + entry.lent_index = g_draws.size(); + } + else if (entry.lent_index < g_draws.size() && g_draws[entry.lent_index].geometry_key == draw.geometry_key && + g_draws[entry.lent_index].geometry_revision == draw.geometry_revision && + !g_draws[entry.lent_index].positions.empty()) + { + draw.shared_geometry = true; + draw.shared_diffuse = !g_draws[entry.lent_index].diffuse.empty(); + } + else + reuse = false; } - else + if (!reuse) { draw.positions.resize(count * 3); if (layout.rhw) draw.rhw.resize(count); @@ -978,6 +991,8 @@ private: if (cached != m_geometry_cache.end()) { cached->second.last_used = frame_id; + if (!cached->second.changing && cached->second.revision == draw.geometry_revision) + cached->second.lent_index = g_draws.size(); // re-extracted: later repeats share this draw if (!cached->second.changing && cached->second.revision != draw.geometry_revision) { cached->second.changing = true; @@ -985,7 +1000,9 @@ private: } } else if (draw.geometry_key && cached == m_geometry_cache.end()) - m_geometry_cache.emplace(draw.geometry_key, GeometryCacheEntry{draw.geometry_revision, frame_id, false, draw}); + // The new entry starts lent to this draw; reclaim() fills it when the next frame begins. + m_geometry_cache.emplace(draw.geometry_key, + GeometryCacheEntry{draw.geometry_revision, frame_id, false, Render3DDraw(), g_draws.size()}); } if (gpu_skinned && skin_view.bone_matrices && skin_view.bone_count > 0) draw.bone_matrices.assign(skin_view.bone_matrices, skin_view.bone_matrices + std::size_t(skin_view.bone_count) * 16); @@ -1091,7 +1108,7 @@ private: { auto* surface = static_cast(m_renderTarget); if (!surface->parent || surface->level != 0 || surface->desc.Format != D3DFMT_R5G6B5 || draw.lines || - draw.pretransformed || draw.indices.size() < 3) + draw.pretransformed || (!draw.shared_geometry && draw.indices.size() < 3)) return; draw.render_target = surface->parent->id; draw.target_width = surface->desc.Width; @@ -1143,7 +1160,42 @@ private: std::uint64_t last_used; bool changing; Render3DDraw geometry; + std::size_t lent_index = SIZE_MAX; // g_draws slot holding the lent vectors this frame }; + + template + static void for_each_geometry_vector(Render3DDraw& a, Render3DDraw& b, Fn fn) + { + fn(a.positions, b.positions); fn(a.rhw, b.rhw); fn(a.vertex_fog, b.vertex_fog); + fn(a.normals, b.normals); fn(a.uv0, b.uv0); fn(a.uv1, b.uv1); fn(a.diffuse, b.diffuse); + fn(a.indices, b.indices); fn(a.bone_indices, b.bone_indices); fn(a.bone_weights, b.bone_weights); + } + static void move_geometry(Render3DDraw& from, Render3DDraw& to) + { + for_each_geometry_vector(from, to, [](auto& a, auto& b) { b = std::move(a); a.clear(); }); + } + +public: + // Render3DBeginFrame, before the last frame's draws are cleared: the cache takes back the vectors + // it lent (or a re-extracted copy) from the draws that carry its geometry. + void reclaim(std::vector& draws) + { + for (auto& draw : draws) + { + if (!draw.geometry_key || draw.positions.empty()) + continue; + auto it = m_geometry_cache.find(draw.geometry_key); + if (it == m_geometry_cache.end()) + continue; + GeometryCacheEntry& entry = it->second; + entry.lent_index = SIZE_MAX; + if (!entry.changing && entry.revision == draw.geometry_revision && entry.geometry.positions.empty()) + move_geometry(draw, entry.geometry); + } + } + +private: + std::vector m_indexScratch; std::unordered_map m_geometry_cache; std::uint64_t m_cache_pruned_frame = 0; std::vector m_offscreenDepth; @@ -1251,6 +1303,8 @@ std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std: void Render3DBeginFrame() { std::lock_guard lock(g_draws_mutex); + if (g_recording_device) + g_recording_device->reclaim(g_draws); g_draws.clear(); } @@ -1266,6 +1320,31 @@ void Render3DEndScene() { g_in_scene = false; } const std::vector& Render3DDraws() { return g_draws; } +void Render3DExpandSharedGeometry(std::vector& draws) +{ + std::unordered_map owners; // geometry_key -> draw carrying the vectors + for (std::size_t i = 0; i < draws.size(); ++i) + { + Render3DDraw& draw = draws[i]; + if (!draw.geometry_key) + continue; + if (!draw.shared_geometry) + { + if (!draw.positions.empty()) + owners[draw.geometry_key] = i; + continue; + } + auto owner = owners.find(draw.geometry_key); + if (owner == owners.end() || draws[owner->second].geometry_revision != draw.geometry_revision) + continue; + const Render3DDraw& from = draws[owner->second]; + draw.positions = from.positions; draw.rhw = from.rhw; draw.vertex_fog = from.vertex_fog; + draw.normals = from.normals; draw.uv0 = from.uv0; draw.uv1 = from.uv1; draw.diffuse = from.diffuse; + draw.indices = from.indices; draw.bone_indices = from.bone_indices; draw.bone_weights = from.bone_weights; + draw.shared_geometry = false; + } +} + // D3D8 fixed-function texture coordinate processing for one stage (the native renderer's // native.vert applies the same rules on the GPU): // - D3DTSS_TEXCOORDINDEX low word picks the vertex set, a 2D set enters the transform as (u, v, 1, 0) diff --git a/src/platform/EterLib/RenderCommands3D.h b/src/platform/EterLib/RenderCommands3D.h index 0b575fd9..efcb1dde 100644 --- a/src/platform/EterLib/RenderCommands3D.h +++ b/src/platform/EterLib/RenderCommands3D.h @@ -61,6 +61,12 @@ struct Render3DDraw { std::vector bone_weights; // 4 floats per vertex std::vector bone_matrices; // 16 floats per bone in the mesh's palette (D3D row-vector layout) bool lines = false; + // A repeat, within one frame, of an earlier draw's geometry (same geometry_key and revision, e.g. + // the terrain splat layers of a patch or a character's shadow-map pass): positions..bone_weights + // are left empty and the consumer takes them from that earlier draw of Render3DDraws(). + // shared_diffuse says whether the geometry has vertex colours (!diffuse.empty() there). + bool shared_geometry = false; + bool shared_diffuse = false; // D3DRS_* / D3DTSS_* values in effect for the draw. std::uint32_t alpha_blend = 0, src_blend = 0, dest_blend = 0; @@ -146,3 +152,6 @@ void Render3DAdd(Render3DDraw draw); void Render3DBeginScene(); void Render3DEndScene(); const std::vector& Render3DDraws(); +// Fills the vectors of shared_geometry draws from the earlier draw that carries them (for consumers +// that need every draw self-contained, e.g. a draw capture). +void Render3DExpandSharedGeometry(std::vector& draws);