feat: add editor viewport host service

This commit is contained in:
2026-03-28 17:04:14 +08:00
parent 6fcb6ac8fb
commit 3b652ac1db
12 changed files with 492 additions and 2 deletions

View File

@@ -68,6 +68,9 @@ void Application::InitializeImGui(HWND hwnd) {
m_imguiBackend,
m_windowRenderer.GetDevice(),
m_windowRenderer.GetCommandQueue());
m_viewportHostService.Initialize(m_imguiBackend, m_windowRenderer.GetRHIDevice());
static_cast<EditorContext*>(m_editorContext.get())->SetViewportHostService(&m_viewportHostService);
}
void Application::AttachEditorLayer() {
@@ -94,10 +97,18 @@ void Application::ShutdownEditorContext() {
void Application::RenderEditorFrame() {
static constexpr float kClearColor[4] = { 0.22f, 0.22f, 0.22f, 1.0f };
m_imguiBackend.BeginFrame();
m_viewportHostService.BeginFrame();
m_layerStack.onImGuiRender();
UpdateWindowTitle();
ImGui::Render();
m_windowRenderer.Render(m_imguiBackend, kClearColor);
m_windowRenderer.Render(
m_imguiBackend,
kClearColor,
[this](const Rendering::RenderContext& renderContext) {
if (m_editorContext) {
m_viewportHostService.RenderRequestedViewports(*m_editorContext, renderContext);
}
});
}
bool Application::Initialize(HWND hwnd) {
@@ -135,6 +146,10 @@ bool Application::Initialize(HWND hwnd) {
void Application::Shutdown() {
m_renderReady = false;
DetachEditorLayer();
if (m_editorContext) {
static_cast<EditorContext*>(m_editorContext.get())->SetViewportHostService(nullptr);
}
m_viewportHostService.Shutdown();
UI::ShutdownBuiltInIcons();
m_imguiBackend.Shutdown();
m_imguiSession.Shutdown();

View File

@@ -3,6 +3,7 @@
#include "Platform/D3D12WindowRenderer.h"
#include "UI/ImGuiBackendBridge.h"
#include "UI/ImGuiSession.h"
#include "Viewport/ViewportHostService.h"
#include <XCEngine/Rendering/RenderContext.h>
#include <memory>
@@ -35,6 +36,7 @@ public:
Rendering::RenderContext GetMainRenderContext() const { return m_windowRenderer.GetRenderContext(); }
RHI::RHIDevice* GetMainRHIDevice() const { return m_windowRenderer.GetRHIDevice(); }
RHI::RHISwapChain* GetMainSwapChain() const { return m_windowRenderer.GetSwapChain(); }
IViewportHostService& GetViewportHostService() { return m_viewportHostService; }
bool IsRenderReady() const { return m_renderReady; }
HWND GetWindowHandle() const { return m_hwnd; }
@@ -60,6 +62,7 @@ private:
Platform::D3D12WindowRenderer m_windowRenderer;
UI::ImGuiBackendBridge m_imguiBackend;
UI::ImGuiSession m_imguiSession;
ViewportHostService m_viewportHostService;
uint64_t m_exitRequestedHandlerId = 0;
std::wstring m_lastWindowTitle;
bool m_renderReady = false;

View File

@@ -10,6 +10,7 @@
#include "Managers/SceneManager.h"
#include "Managers/ProjectManager.h"
#include "EditorEvents.h"
#include "Viewport/IViewportHostService.h"
#include <string>
#include <memory>
@@ -56,6 +57,14 @@ public:
return *m_undoManager;
}
IViewportHostService* GetViewportHostService() override {
return m_viewportHostService;
}
void SetViewportHostService(IViewportHostService* viewportHostService) {
m_viewportHostService = viewportHostService;
}
void SetActiveActionRoute(EditorActionRoute route) override {
m_activeActionRoute = route;
}
@@ -78,6 +87,7 @@ private:
std::unique_ptr<SceneManager> m_sceneManager;
std::unique_ptr<UndoManager> m_undoManager;
std::unique_ptr<ProjectManager> m_projectManager;
IViewportHostService* m_viewportHostService = nullptr;
EditorActionRoute m_activeActionRoute = EditorActionRoute::None;
std::string m_projectPath;
uint64_t m_entityDeletedHandlerId;

View File

@@ -13,6 +13,7 @@ class ISelectionManager;
class IProjectManager;
class ISceneManager;
class IUndoManager;
class IViewportHostService;
class IEditorContext {
public:
@@ -23,6 +24,7 @@ public:
virtual ISceneManager& GetSceneManager() = 0;
virtual IProjectManager& GetProjectManager() = 0;
virtual IUndoManager& GetUndoManager() = 0;
virtual IViewportHostService* GetViewportHostService() = 0;
virtual void SetActiveActionRoute(EditorActionRoute route) = 0;
virtual EditorActionRoute GetActiveActionRoute() const = 0;

View File

@@ -6,6 +6,10 @@
#include <XCEngine/Components/GameObject.h>
namespace XCEngine {
namespace Components {
class Scene;
}
namespace Editor {
class ISceneManager {
@@ -32,6 +36,8 @@ public:
virtual void MarkSceneDirty() = 0;
virtual const std::string& GetCurrentScenePath() const = 0;
virtual const std::string& GetCurrentSceneName() const = 0;
virtual ::XCEngine::Components::Scene* GetScene() = 0;
virtual const ::XCEngine::Components::Scene* GetScene() const = 0;
virtual void CreateDemoScene() = 0;
};

View File

@@ -59,6 +59,8 @@ public:
void MarkSceneDirty() override;
const std::string& GetCurrentScenePath() const override { return m_currentScenePath; }
const std::string& GetCurrentSceneName() const override { return m_currentSceneName; }
::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); }
const ::XCEngine::Components::Scene* GetScene() const override { return m_scene.get(); }
void CreateDemoScene() override;
bool HasClipboardData() const { return m_clipboard.has_value(); }

View File

@@ -16,6 +16,7 @@
#include <XCEngine/RHI/D3D12/D3D12SwapChain.h>
#include <d3d12.h>
#include <functional>
#include <vector>
#include <windows.h>
@@ -154,7 +155,10 @@ public:
RecreateBackBufferViews();
}
void Render(UI::ImGuiBackendBridge& imguiBackend, const float clearColor[4]) {
void Render(
UI::ImGuiBackendBridge& imguiBackend,
const float clearColor[4],
const std::function<void(const Rendering::RenderContext&)>& beforeUiRender = {}) {
auto* d3d12Queue = GetD3D12CommandQueue();
auto* d3d12CommandList = GetD3D12CommandList();
if (m_swapChain == nullptr ||
@@ -167,6 +171,10 @@ public:
d3d12Queue->WaitForPreviousFrame();
m_commandList->Reset();
if (beforeUiRender) {
beforeUiRender(GetRenderContext());
}
const uint32_t backBufferIndex = m_swapChain->GetCurrentBackBufferIndex();
if (backBufferIndex >= m_backBufferViews.size() || m_backBufferViews[backBufferIndex] == nullptr) {
return;

View File

@@ -0,0 +1,42 @@
#pragma once
#include <imgui.h>
#include <string>
namespace XCEngine {
namespace Rendering {
struct RenderContext;
} // namespace Rendering
namespace Editor {
class IEditorContext;
enum class EditorViewportKind {
Scene,
Game
};
struct EditorViewportFrame {
ImTextureID textureId = {};
ImVec2 requestedSize = ImVec2(0.0f, 0.0f);
ImVec2 renderSize = ImVec2(0.0f, 0.0f);
bool hasTexture = false;
bool wasRequested = false;
std::string statusText;
};
class IViewportHostService {
public:
virtual ~IViewportHostService() = default;
virtual void BeginFrame() = 0;
virtual EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) = 0;
virtual void RenderRequestedViewports(
IEditorContext& context,
const Rendering::RenderContext& renderContext) = 0;
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,338 @@
#pragma once
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "IViewportHostService.h"
#include "UI/ImGuiBackendBridge.h"
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/RHI/D3D12/D3D12Device.h>
#include <XCEngine/RHI/D3D12/D3D12Texture.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <XCEngine/RHI/RHITexture.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/Rendering/SceneRenderer.h>
#include <XCEngine/Scene/Scene.h>
#include <array>
#include <cstdint>
#include <memory>
#include <string>
namespace XCEngine {
namespace Editor {
class ViewportHostService : public IViewportHostService {
public:
void Initialize(UI::ImGuiBackendBridge& backend, RHI::RHIDevice* device) {
Shutdown();
m_backend = &backend;
m_device = device ? static_cast<RHI::D3D12Device*>(device) : nullptr;
}
void Shutdown() {
for (ViewportEntry& entry : m_entries) {
DestroyViewportResources(entry);
entry = {};
}
m_device = nullptr;
m_backend = nullptr;
m_sceneRenderer.reset();
}
void BeginFrame() override {
for (ViewportEntry& entry : m_entries) {
entry.requestedThisFrame = false;
entry.requestedWidth = 0;
entry.requestedHeight = 0;
}
}
EditorViewportFrame RequestViewport(EditorViewportKind kind, const ImVec2& requestedSize) override {
ViewportEntry& entry = GetEntry(kind);
entry.requestedThisFrame = requestedSize.x > 1.0f && requestedSize.y > 1.0f;
entry.requestedWidth = entry.requestedThisFrame
? static_cast<uint32_t>(requestedSize.x)
: 0u;
entry.requestedHeight = entry.requestedThisFrame
? static_cast<uint32_t>(requestedSize.y)
: 0u;
if (entry.requestedThisFrame && m_backend != nullptr && m_device != nullptr) {
EnsureViewportResources(entry);
}
EditorViewportFrame frame = {};
frame.textureId = entry.textureId;
frame.requestedSize = requestedSize;
frame.renderSize = ImVec2(static_cast<float>(entry.width), static_cast<float>(entry.height));
frame.hasTexture = entry.textureId != ImTextureID{};
frame.wasRequested = entry.requestedThisFrame;
frame.statusText = entry.statusText;
return frame;
}
void RenderRequestedViewports(
IEditorContext& context,
const Rendering::RenderContext& renderContext) override {
if (m_backend == nullptr || m_device == nullptr || !renderContext.IsValid()) {
return;
}
EnsureSceneRenderer();
const auto* scene = context.GetSceneManager().GetScene();
for (ViewportEntry& entry : m_entries) {
if (!entry.requestedThisFrame) {
continue;
}
if (!EnsureViewportResources(entry)) {
entry.statusText = "Failed to create viewport render targets";
continue;
}
RenderViewportEntry(entry, scene, renderContext);
}
}
private:
struct ViewportEntry {
EditorViewportKind kind = EditorViewportKind::Scene;
uint32_t width = 0;
uint32_t height = 0;
uint32_t requestedWidth = 0;
uint32_t requestedHeight = 0;
bool requestedThisFrame = false;
RHI::RHITexture* colorTexture = nullptr;
RHI::RHIResourceView* colorView = nullptr;
RHI::RHITexture* depthTexture = nullptr;
RHI::RHIResourceView* depthView = nullptr;
D3D12_CPU_DESCRIPTOR_HANDLE imguiCpuHandle = {};
D3D12_GPU_DESCRIPTOR_HANDLE imguiGpuHandle = {};
ImTextureID textureId = {};
RHI::ResourceStates colorState = RHI::ResourceStates::Common;
std::string statusText;
};
ViewportEntry& GetEntry(EditorViewportKind kind) {
const size_t index = kind == EditorViewportKind::Scene ? 0u : 1u;
m_entries[index].kind = kind;
return m_entries[index];
}
void EnsureSceneRenderer() {
if (!m_sceneRenderer) {
m_sceneRenderer = std::make_unique<Rendering::SceneRenderer>();
}
}
bool EnsureViewportResources(ViewportEntry& entry) {
if (entry.requestedWidth == 0 || entry.requestedHeight == 0) {
return false;
}
if (entry.width == entry.requestedWidth &&
entry.height == entry.requestedHeight &&
entry.colorTexture != nullptr &&
entry.colorView != nullptr &&
entry.depthTexture != nullptr &&
entry.depthView != nullptr &&
entry.textureId != ImTextureID{}) {
return true;
}
DestroyViewportResources(entry);
entry.width = entry.requestedWidth;
entry.height = entry.requestedHeight;
RHI::TextureDesc colorDesc = {};
colorDesc.width = entry.width;
colorDesc.height = entry.height;
colorDesc.depth = 1;
colorDesc.mipLevels = 1;
colorDesc.arraySize = 1;
colorDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
colorDesc.textureType = static_cast<uint32_t>(RHI::TextureType::Texture2D);
colorDesc.sampleCount = 1;
colorDesc.sampleQuality = 0;
colorDesc.flags = 0;
entry.colorTexture = m_device->CreateTexture(colorDesc);
if (entry.colorTexture == nullptr) {
DestroyViewportResources(entry);
return false;
}
RHI::ResourceViewDesc colorViewDesc = {};
colorViewDesc.format = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
colorViewDesc.dimension = RHI::ResourceViewDimension::Texture2D;
entry.colorView = m_device->CreateRenderTargetView(entry.colorTexture, colorViewDesc);
if (entry.colorView == nullptr) {
DestroyViewportResources(entry);
return false;
}
RHI::TextureDesc depthDesc = {};
depthDesc.width = entry.width;
depthDesc.height = entry.height;
depthDesc.depth = 1;
depthDesc.mipLevels = 1;
depthDesc.arraySize = 1;
depthDesc.format = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
depthDesc.textureType = static_cast<uint32_t>(RHI::TextureType::Texture2D);
depthDesc.sampleCount = 1;
depthDesc.sampleQuality = 0;
depthDesc.flags = 0;
entry.depthTexture = m_device->CreateTexture(depthDesc);
if (entry.depthTexture == nullptr) {
DestroyViewportResources(entry);
return false;
}
RHI::ResourceViewDesc depthViewDesc = {};
depthViewDesc.format = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
depthViewDesc.dimension = RHI::ResourceViewDimension::Texture2D;
entry.depthView = m_device->CreateDepthStencilView(entry.depthTexture, depthViewDesc);
if (entry.depthView == nullptr) {
DestroyViewportResources(entry);
return false;
}
m_backend->AllocateTextureDescriptor(&entry.imguiCpuHandle, &entry.imguiGpuHandle);
if (entry.imguiCpuHandle.ptr == 0 || entry.imguiGpuHandle.ptr == 0) {
DestroyViewportResources(entry);
return false;
}
auto* nativeTexture = static_cast<RHI::D3D12Texture*>(entry.colorTexture);
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
m_device->GetDevice()->CreateShaderResourceView(
nativeTexture->GetResource(),
&srvDesc,
entry.imguiCpuHandle);
entry.textureId = static_cast<ImTextureID>(entry.imguiGpuHandle.ptr);
entry.colorState = RHI::ResourceStates::Common;
return true;
}
void RenderViewportEntry(
ViewportEntry& entry,
const Components::Scene* scene,
const Rendering::RenderContext& renderContext) {
if (entry.colorView == nullptr || entry.depthView == nullptr) {
entry.statusText = "Viewport render target is unavailable";
return;
}
if (scene == nullptr) {
entry.statusText = "No active scene";
ClearViewport(entry, renderContext, 0.07f, 0.08f, 0.10f, 1.0f);
return;
}
const auto cameras = scene->FindObjectsOfType<Components::CameraComponent>();
if (cameras.empty()) {
entry.statusText = "No camera in scene";
ClearViewport(entry, renderContext, 0.10f, 0.09f, 0.08f, 1.0f);
return;
}
Rendering::RenderSurface surface(entry.width, entry.height);
surface.SetColorAttachment(entry.colorView);
surface.SetDepthAttachment(entry.depthView);
surface.SetColorStateBefore(entry.colorState);
surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource);
if (!m_sceneRenderer->Render(*scene, nullptr, renderContext, surface)) {
entry.statusText = "Scene renderer failed";
ClearViewport(entry, renderContext, 0.18f, 0.07f, 0.07f, 1.0f);
return;
}
entry.colorState = RHI::ResourceStates::PixelShaderResource;
entry.statusText.clear();
}
void ClearViewport(
ViewportEntry& entry,
const Rendering::RenderContext& renderContext,
float r,
float g,
float b,
float a) {
RHI::RHICommandList* commandList = renderContext.commandList;
if (commandList == nullptr) {
entry.statusText = "Viewport command list is unavailable";
return;
}
const float clearColor[4] = { r, g, b, a };
RHI::RHIResourceView* colorView = entry.colorView;
commandList->TransitionBarrier(
colorView,
entry.colorState,
RHI::ResourceStates::RenderTarget);
commandList->SetRenderTargets(1, &colorView, entry.depthView);
commandList->ClearRenderTarget(colorView, clearColor);
commandList->ClearDepthStencil(entry.depthView, 1.0f, 0);
commandList->TransitionBarrier(
colorView,
RHI::ResourceStates::RenderTarget,
RHI::ResourceStates::PixelShaderResource);
entry.colorState = RHI::ResourceStates::PixelShaderResource;
}
void DestroyViewportResources(ViewportEntry& entry) {
if (m_backend != nullptr && entry.imguiCpuHandle.ptr != 0) {
m_backend->FreeTextureDescriptor(entry.imguiCpuHandle, entry.imguiGpuHandle);
}
if (entry.depthView != nullptr) {
entry.depthView->Shutdown();
delete entry.depthView;
entry.depthView = nullptr;
}
if (entry.depthTexture != nullptr) {
entry.depthTexture->Shutdown();
delete entry.depthTexture;
entry.depthTexture = nullptr;
}
if (entry.colorView != nullptr) {
entry.colorView->Shutdown();
delete entry.colorView;
entry.colorView = nullptr;
}
if (entry.colorTexture != nullptr) {
entry.colorTexture->Shutdown();
delete entry.colorTexture;
entry.colorTexture = nullptr;
}
entry.width = 0;
entry.height = 0;
entry.imguiCpuHandle = {};
entry.imguiGpuHandle = {};
entry.textureId = {};
entry.colorState = RHI::ResourceStates::Common;
}
UI::ImGuiBackendBridge* m_backend = nullptr;
RHI::D3D12Device* m_device = nullptr;
std::unique_ptr<Rendering::SceneRenderer> m_sceneRenderer;
std::array<ViewportEntry, 2> m_entries = {};
};
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,5 +1,6 @@
#include "Actions/ActionRouting.h"
#include "GameViewPanel.h"
#include "ViewportPanelContent.h"
#include "UI/UI.h"
#include <imgui.h>
@@ -14,6 +15,7 @@ void GameViewPanel::Render() {
return;
}
RenderViewportPanelContent(*m_context, EditorViewportKind::Game);
Actions::ObserveInactiveActionRoute(*m_context);
}

View File

@@ -1,5 +1,6 @@
#include "Actions/ActionRouting.h"
#include "SceneViewPanel.h"
#include "ViewportPanelContent.h"
#include "UI/UI.h"
#include <imgui.h>
@@ -14,6 +15,7 @@ void SceneViewPanel::Render() {
return;
}
RenderViewportPanelContent(*m_context, EditorViewportKind::Scene);
Actions::ObserveInactiveActionRoute(*m_context);
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include "Core/IEditorContext.h"
#include "Viewport/IViewportHostService.h"
#include <imgui.h>
#include <string>
namespace XCEngine {
namespace Editor {
inline void DrawViewportStatusMessage(const std::string& message) {
if (message.empty()) {
return;
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (drawList == nullptr) {
return;
}
const ImVec2 min = ImGui::GetItemRectMin();
const ImVec2 max = ImGui::GetItemRectMax();
const ImVec2 textSize = ImGui::CalcTextSize(message.c_str());
const ImVec2 textPos(
min.x + ((max.x - min.x) - textSize.x) * 0.5f,
min.y + ((max.y - min.y) - textSize.y) * 0.5f);
drawList->AddText(textPos, ImGui::GetColorU32(ImGuiCol_TextDisabled), message.c_str());
}
inline void RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) {
IViewportHostService* viewportHostService = context.GetViewportHostService();
const ImVec2 availableSize = ImGui::GetContentRegionAvail();
if (availableSize.x <= 1.0f || availableSize.y <= 1.0f) {
ImGui::Dummy(ImVec2(0.0f, 0.0f));
return;
}
if (viewportHostService == nullptr) {
ImGui::Dummy(availableSize);
DrawViewportStatusMessage("Viewport host is unavailable");
return;
}
const EditorViewportFrame frame = viewportHostService->RequestViewport(kind, availableSize);
if (frame.hasTexture) {
ImGui::Image(frame.textureId, availableSize);
DrawViewportStatusMessage(frame.statusText);
return;
}
ImGui::Dummy(availableSize);
DrawViewportStatusMessage(
frame.statusText.empty() ? "Viewport is initializing" : frame.statusText);
}
} // namespace Editor
} // namespace XCEngine