Compare commits

...
2 Commits
Author SHA1 Message Date
atdunbg 42757bab36 AssetManager: drop immediate SaveGUIDRegistry on registration
2268fa5 added the dirty-flag deferred flush but left the immediate
save in place, so new GUID registrations wrote the registry twice
(once inline, once at the next FlushGUIDRegistry). Remove the inline
save so the deferred path is the only writer.
2026-10-05 00:39:48 +08:00
atdunbg dec171cd1e Fix selection outline never showing for the first entity
entt entity 0's integral bits are 0, colliding with the "no selection"
sentinel: u32 selected == 0 meant "clear selection", so the scene's
first-created entity could never be outlined. Replaced with
std::optional<u32> + FrameSceneData::selectedEntityValid.

Also restore per-batch instance contiguity broken by the batchMats
flattening (e038cb3): entities sharing a mesh scatter across the flat
instance array in collection order. ViewScratch now tracks per-batch
singly-linked lists and appendView aggregates each batch into a
contiguous range, preserving the flat-array performance intent.

Hierarchy click selection now routes through SelectEntity so outline
stays in sync when picking from the panel.
2026-10-05 00:39:48 +08:00
9 changed files with 98 additions and 42 deletions
+10 -1
View File
@@ -131,7 +131,16 @@ void Editor::Draw()
ImGui::End();
// 常规面板窗口(窗口名 = docking 身份)
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
if (m_ShowHierarchy)
{
const entt::entity prev = m_Selected;
m_Hierarchy->Draw(m_Ctx, m_Selected);
// 树点选同步描边(面板直写 m_Selected,不走 Viewport 的
// SelectEntity 返回值通道;对比前后变化触发)
if (m_Selected != prev) {
SelectEntity(m_Selected);
}
}
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport)
{
+7 -4
View File
@@ -21,6 +21,7 @@
#include <entt/entt.hpp>
#include <memory>
#include <optional>
class HierarchyPanel;
class InspectorPanel;
@@ -67,16 +68,18 @@ private:
void DoStop(); // TogglePlay 的 Stop 半程(恢复 + 回编辑器 + 冻结)
// ---- 选中状态 ----
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体
//(bits = 实体位模式,0 = 清选中)
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。
// nullopt = 清选中——实体 0 号(场景首个创建)位模式 = 0,
// 不能拿位值兼作哨兵("第一个模型无描边"的根因)
void SelectEntity(entt::entity e)
{
m_Selected = e;
if (m_Ctx.renderer) {
m_Ctx.renderer->SetSelectedEntity(
e == entt::null
? 0u
: static_cast<Engine::u32>(entt::to_integral(e)));
? std::optional<Engine::u32>{}
: std::optional<Engine::u32>(
static_cast<Engine::u32>(entt::to_integral(e))));
}
}
entt::entity GetSelected() const { return m_Selected; }
+4 -3
View File
@@ -164,9 +164,10 @@ namespace Engine {
std::optional<u32> TakePickResult();
// ---- Editor 选中描边(UE 编辑器选中轮廓)----
// UI 段选中变化时调用(bits = 实体位模式,0 = 清选中);
// 下一帧 SelectionMask 只画该实体,Outline 在其轮廓叠加描边色
void SetSelectedEntity(u32 entityBits);
// UI 段选中变化时调用;下一帧 SelectionMask 只画该实体,Outline
// 在其轮廓叠加描边色。nullopt = 清选中(实体 0 号位模式 = 0,
// 不能拿位值兼作哨兵)
void SetSelectedEntity(std::optional<u32> entityBits);
// 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前)
+19 -6
View File
@@ -26,6 +26,7 @@
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <optional>
#include <unordered_map>
#include <Engine/Renderer/EditorCamera.h>
@@ -73,8 +74,12 @@ namespace Engine {
{
m_PendingPick = PickRequest{true, ndc};
}
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制
void SetSelectedEntity(u32 entityBits) { m_PendingSelected = entityBits; }
// Editor 选中实体(描边):UI 段写入,BuildFrame 落帧并筛出绘制。
// nullopt = 清选中(实体 0 号位模式 = 0,不能拿位值兼作哨兵)
void SetSelectedEntity(std::optional<u32> entityBits)
{
m_PendingSelected = entityBits;
}
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
@@ -91,7 +96,7 @@ namespace Engine {
FrameSceneData m_Frame;
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
u32 m_PendingSelected = 0; // 选中实体缓冲(描边)
std::optional<u32> m_PendingSelected; // 选中实体缓冲(nullopt = 无选中)
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
struct ViewScratch {
@@ -102,12 +107,18 @@ namespace Engine {
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
u32 entityBits = 0; // 实体位模式(HitProxy id pass 消费)
};
// 链表尾哨兵:kMaxInstances 远小于 u32 最大值,安全
static constexpr u32 kNoInstance = 0xFFFFFFFFu;
CullResult result;
// 扁平实例数组(所有 batch 的实例共用一份连续内存)。
// InstanceBatch::first/count 在本数组里切片——大场景下避免
// vector<vector> 的小块分配(每个 batch 自己一个 vector,push_back
// 触发各自重分配,cache miss 严重)。每帧 clear() 复用容量
// 实体收集序下同 batch 实例会散布(多实体共享 mesh 交错插入),
// 组内连接关系由 instanceNext 链表维护——appendView 时按 batch
// 聚合拷成连续区间(firstInstance 连续语义的前提)。大场景下
// 避免 vector<vector> 的小块分配,每帧 clear() 复用容量
std::vector<InstanceMatrices> instances;
std::vector<u32> instanceNext; // 与 instances 平行:同 batch 下一实例
// 每 batch 实例链头(与 result.batches 平行;kNoInstance = 空)
std::vector<u32> batchHead;
// 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
std::vector<f32> batchDistSum;
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
@@ -119,6 +130,8 @@ namespace Engine {
result.batches.clear();
result.instanceCount = 0;
instances.clear(); // 容量保留,复用
instanceNext.clear();
batchHead.clear();
batchDistSum.clear();
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
}
+4 -1
View File
@@ -167,9 +167,12 @@ namespace Engine {
// HitProxy/HitProxyCopy 两 pass 据此决定是否绘制/拷贝)
PickRequest pickRequest{};
// 选中描边(UE 编辑器语义):选中实体的位模式(0 = 无选中)。
// 选中描边(UE 编辑器语义):选中实体 + 位模式。位模式不能兼作
// "无选中"哨兵——entt 首个实体 0 号的 to_integral == 0("点击
// 场景第一个创建的实体没有描边"的根因),有效性与位值解耦。
// BuildFrame 据此从实例表筛出 selectionDraws,SelectionMask pass
// 只画选中实体 → Outline pass 在 mask 边缘叠加描边色
bool selectedEntityValid = false;
u32 selectedEntityBits = 0;
std::vector<SelectionDraw> selectionDraws;
+1 -2
View File
@@ -49,8 +49,7 @@ namespace Engine::Assets {
m_PathToGuid.emplace(path, guid);
m_GuidToPath.emplace(guid, path);
SaveGUIDRegistry();
m_GuidRegistryDirty = true;
m_GuidRegistryDirty = true; // 延迟落盘:由 FlushGUIDRegistry 统一写
return guid;
}
+3 -2
View File
@@ -38,8 +38,9 @@ namespace Engine::Passes
void OutlinePass::Register(RG::RenderGraph& rg)
{
rg.AddPass("Outline", [this](RG::RenderGraphContext& ctx) {
// 无选中零 GPU(mask 已由 SelectionMask pass 清 0,无描边)
if (m_Frame.selectedEntityBits == 0) return;
// 无选中零 GPU(mask 已由 SelectionMask pass 清 0,无描边)。
// 有效位判断:实体 0 号位模式 = 0,不能拿位值当哨兵
if (!m_Frame.selectedEntityValid) return;
const vk::CommandBuffer cmd = ctx.cmd;
+1 -1
View File
@@ -251,7 +251,7 @@ namespace Engine
m_SceneRenderer->SetPickRequest(ndc);
}
void Renderer::SetSelectedEntity(u32 entityBits)
void Renderer::SetSelectedEntity(std::optional<u32> entityBits)
{
m_SceneRenderer->SetSelectedEntity(entityBits);
}
+49 -22
View File
@@ -289,8 +289,10 @@ namespace Engine
// 放在相机校验之后:跳帧时请求保留,不丢点击
m_Frame.pickRequest = m_PendingPick;
m_PendingPick.active = false;
// 选中实体落帧(持续状态,不做一次性清除)
m_Frame.selectedEntityBits = m_PendingSelected;
// 选中实体落帧(持续状态,不做一次性清除)。
// 有效性与位值分离:0 号实体(场景首个创建)位模式 = 0
m_Frame.selectedEntityValid = m_PendingSelected.has_value();
m_Frame.selectedEntityBits = m_PendingSelected.value_or(0);
const f32 aspect = static_cast<f32>(targetExtent.width) /
static_cast<f32>(targetExtent.height);
@@ -613,18 +615,23 @@ namespace Engine
if (batchIdx == 0) {
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
lodSlots[lod] = batchIdx;
// first = 当前 instances 数组大小(batch 还没实例)
// push_back 实例后区间自然连续
s.result.batches.push_back(
{mesh.get(), static_cast<u32>(s.instances.size()), 0, lod});
// first 是占位:真实基址在 appendView 聚合时
// 回填(收集序下 batch 实例散布,见链表注释)
s.result.batches.push_back({mesh.get(), 0, 0, lod});
s.batchDistSum.push_back(0.0f);
s.batchHead.push_back(ViewScratch::kNoInstance);
}
// 实例归入自己的 batch(mesh 分组),保证同 batch
// 实例在扁平数组里连续——firstInstance 语义的前提
// 实例归入自己的 batch(mesh 分组):收集序散布,
// 组内连接用链表维护(O(1) push,无 per-batch 分配)
const u32 bi = batchIdx - 1;
s.result.batches[bi].count++;
const u32 ii =
static_cast<u32>(s.instances.size());
s.instances.push_back(
{t.World(), prevWorld, skinBase, entKey}); // 单 vector push
{t.World(), prevWorld, skinBase, entKey});
s.instanceNext.push_back(s.batchHead[bi]);
s.batchHead[bi] = ii; // 头插:链序 = push 逆序(无妨,
// 实例对齐性由 (矩阵,id) 成对保证)
s.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++;
++totalInstances;
@@ -657,15 +664,16 @@ namespace Engine
dst.batches.reserve(s.result.batches.size());
for (size_t i = 0; i < s.result.batches.size(); ++i) {
InstanceBatch b = s.result.batches[i];
// ✅ 保存原 first(s.instances 数组里的下标),b.first 改成
// m_Frame 数组里的基址
const u32 srcFirst = b.first;
b.first = base; // 扁平化后的连续区间基址
base += b.count;
dst.batches.push_back(b);
// 按 batch 的 (srcFirst, count) 切片扁平数组
for (u32 j = 0; j < b.count; ++j) {
const auto& m = s.instances[srcFirst + j];
// 链表遍历:收集序下同 batch 实例散布在扁平数组各处
//(多实体共享 mesh 交错插入),按组聚合拷成连续区间
//——firstInstance 连续语义的前提
for (u32 idx = s.batchHead[i];
idx != ViewScratch::kNoInstance;
idx = s.instanceNext[idx]) {
const auto& m = s.instances[idx];
m_Frame.instanceModels.push_back(m.cur);
m_Frame.prevInstanceModels.push_back(m.prev);
// 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
@@ -688,10 +696,9 @@ namespace Engine
// PackIndirect(m_Frame.shadowCascades[c], false);
// }
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排。
// 扁平 instances 数组按 order 顺序构建新数组(每个 batch 的
// (first,count) 切片搬过去,新 first = 当前已搬数量)----
// 实例按链表逐 batch 搬运(收集序散布,不能按 (first,count) 切片);
// 搬完的新数组天然按 batch 连续 → 链表确定性重建(顺序遍历)
{
auto& tb = m_TranslucentScratch.result.batches;
if (tb.size() > 1) {
@@ -704,11 +711,15 @@ namespace Engine
// 临时构建(容量保留由 swap 进 m_TranslucentScratch 复用)
std::vector<InstanceBatch> sortedBatches;
std::vector<ViewScratch::InstanceMatrices> sortedInstances;
std::vector<u32> sortedNext;
std::vector<u32> sortedHead;
std::vector<f32> sortedDist;
sortedBatches.reserve(tb.size());
sortedHead.reserve(tb.size());
sortedDist.reserve(tb.size());
// 同场景实例总数不变,capacity 可直接复用(reserve 不浪费)
sortedInstances.reserve(m_TranslucentScratch.instances.size());
sortedNext.reserve(m_TranslucentScratch.instances.size());
for (const u32 i : order) {
// 新 batch first = 已搬过去的新 instances 数量
@@ -716,13 +727,29 @@ namespace Engine
b.first = static_cast<u32>(sortedInstances.size());
sortedBatches.push_back(b);
sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]);
// 按 (原 first, count) 切片搬运原扁平数组
for (u32 j = 0; j < tb[i].count; ++j) {
// 链表逐实例搬运;新布局按 batch 连续 → next = 顺序+1。
// 新实例下标 = b.first + n(n = 本 batch 已搬数量),
// 前一个实例的 next 指向它
u32 prev = ViewScratch::kNoInstance;
u32 n = 0;
for (u32 idx = m_TranslucentScratch.batchHead[i];
idx != ViewScratch::kNoInstance;
idx = m_TranslucentScratch.instanceNext[idx]) {
sortedInstances.push_back(std::move(
m_TranslucentScratch.instances[tb[i].first + j]));
m_TranslucentScratch.instances[idx]));
sortedNext.push_back(ViewScratch::kNoInstance);
if (prev != ViewScratch::kNoInstance) {
sortedNext[prev] = b.first + n;
}
prev = b.first + n;
++n;
}
sortedHead.push_back(
n > 0 ? b.first : ViewScratch::kNoInstance);
}
m_TranslucentScratch.result.batches = std::move(sortedBatches);
m_TranslucentScratch.batchHead = std::move(sortedHead);
m_TranslucentScratch.instanceNext = std::move(sortedNext);
m_TranslucentScratch.batchDistSum = std::move(sortedDist);
m_TranslucentScratch.instances = std::move(sortedInstances);
// instanceCount 不变(重排不丢实例)
@@ -734,7 +761,7 @@ namespace Engine
//(不透明 + 透明队列都查——选中玻璃也要有描边)。
// 无选中时保持空表,SelectionMask pass 只 clear 不绘制
m_Frame.selectionDraws.clear();
if (m_Frame.selectedEntityBits != 0) {
if (m_Frame.selectedEntityValid) {
auto collect = [&](const CullResult& view) {
for (const auto& b : view.batches) {
for (u32 i = 0; i < b.count; ++i) {