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 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-30 11:39:01 +09:00
co-authored by Claude Opus 5.5
parent 44a020f01e
commit 6bfcac7d68
5 changed files with 128 additions and 29 deletions
+3 -1
View File
@@ -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<Render3DDraw> 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) {
+1 -1
View File
@@ -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};
+12 -3
View File
@@ -8,6 +8,7 @@
#include <cstring>
#include <fstream>
#include <stdexcept>
#include <tuple>
#include <unordered_set>
namespace mt_host {
@@ -191,14 +192,22 @@ void VulkanWindow::prepare_draws(const std::vector<Render3DDraw>& draws, const T
back_buffer_list(in_scene).push_back(clear);
continue;
}
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);
const GeometryId id{draw.geometry_key, signature};
auto [it, inserted] = geometries_.try_emplace(id);
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_;
+100 -21
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iterator>
@@ -15,6 +16,8 @@
namespace {
std::mutex g_draws_mutex;
std::vector<Render3DDraw> 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<std::uint32_t> 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<std::uint32_t> 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;
}
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<CpuSurface*>(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 <typename Fn>
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<Render3DDraw>& 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<std::uint32_t> m_indexScratch;
std::unordered_map<std::uint64_t, GeometryCacheEntry> m_geometry_cache;
std::uint64_t m_cache_pruned_frame = 0;
std::vector<float> m_offscreenDepth;
@@ -1251,6 +1303,8 @@ std::string Render3DRenderTargetName(std::uint32_t id, std::uint32_t width, std:
void Render3DBeginFrame()
{
std::lock_guard<std::mutex> 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<Render3DDraw>& Render3DDraws() { return g_draws; }
void Render3DExpandSharedGeometry(std::vector<Render3DDraw>& draws)
{
std::unordered_map<std::uint64_t, std::size_t> 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)
+9
View File
@@ -61,6 +61,12 @@ struct Render3DDraw {
std::vector<float> bone_weights; // 4 floats per vertex
std::vector<float> 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<Render3DDraw>& 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<Render3DDraw>& draws);