Skip to content

Commit fa3eae6

Browse files
committed
finish runtime editor boundary migration
1 parent 6135c54 commit fa3eae6

21 files changed

Lines changed: 372 additions & 124 deletions

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ cmake -S . -B Build
6969
7070
# 构建与运行
7171
cmake --build Build --config Debug
72-
& .\Build\Debug\WaveEngine.exe
72+
& .\Build\Debug\WaveEditor.exe
7373
```
7474

7575
`Rebuild.bat` 复用 `Build/`,不是 clean rebuild。Visual Studio 产物在 `Build/<Config>/`;构建后复制 `Engine/ThirdParty/bin/` 的运行库。
@@ -96,8 +96,8 @@ ctest --test-dir Build -C Debug --output-on-failure
9696
| Release 语义、assert | `ctest --test-dir Build -C Release -R CoreRuntimeContracts --output-on-failure` |
9797
| RenderGraph、Pass | Debug 下设置 `WAVE_RG_STRICT=1`,验证真实光栅路径和日志 |
9898
| Path Tracing | 仅在 DXR Tier 1.1 硬件验证;硬件不足明确记为未运行 |
99-
| MCP framing、数值或资产输入 | `py -3 Tests/mcp_asset_input_contracts.py Build/Debug/WaveEngine.exe` |
100-
| WEMesh、模型依赖、缓存 | `py -3 Tests/wemesh_cache_contracts.py Build/Debug/WaveEngine.exe` |
99+
| MCP framing、数值或资产输入 | `py -3 Tests/mcp_asset_input_contracts.py Build/Debug/WaveEditor.exe` |
100+
| WEMesh、模型依赖、缓存 | `py -3 Tests/wemesh_cache_contracts.py Build/Debug/WaveEditor.exe` |
101101
| 坐标、Rotator、相机、导入或序列化 | 同时运行 Core Runtime 与 WEMesh 契约 |
102102
| RHI 异常/关闭 | Debug 设置 `WAVE_RHI_INJECT_FAILURE=1`,应经 quarantine 非零退出 |
103103
| Profiler | Start/Stop、`.wetrace`、live、Viewer、多帧分析、dropped/overflow |
@@ -110,7 +110,7 @@ ctest --test-dir Build -C Debug --output-on-failure
110110

111111
### 生命周期、RHI 与线程
112112

113-
- 入口为 `Engine/Source/WaveEngine.cpp`。启动顺序:Log → Window → Input → RHI/worker → ShaderMap → Scene → Renderer/UI → MCP。
113+
- 入口为 `Engine/Source/WaveEditor.cpp`。启动顺序:Log → Window → Input → RHI/worker → ShaderMap → Scene → Renderer/UI → MCP。
114114
- 主循环顺序:Input → MCP main-thread drain → Scene Tick → Renderer。MCP drain 不依赖窗口可渲染状态。
115115
- MCP 只能在 Renderer/UI 完成工具注册后启动。关闭顺序:拒绝新任务 → 停 MCP → 等 RHI/GPU idle → 销毁 GPU-backed UI/Renderer/cache → Scene/Shader/Input → `FDynamicRHI::Destroy()` → Window。
116116
- Renderer 必须跨过顶层异常保持存活。无法证明 GPU idle 时停止 worker 并经 `_Exit` quarantine,不能执行不安全 GPU teardown。

CMakeLists.txt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ list(APPEND WAVE_EDITOR_SUPPORT_SOURCE
6666
"${PROJECT_SOURCE_DIR}/Engine/Source/Renderer/EditorCompositePass.cpp"
6767
"${PROJECT_SOURCE_DIR}/Engine/Source/Renderer/EditorSelectionPass.cpp")
6868
set(WAVE_EDITOR_ENTRY
69-
"${PROJECT_SOURCE_DIR}/Engine/Source/WaveEngine.cpp")
69+
"${PROJECT_SOURCE_DIR}/Engine/Source/WaveEditor.cpp")
7070

7171
file(GLOB_RECURSE WAVE_RUNTIME_THIRD_PARTY_FILES CONFIGURE_DEPENDS
7272
"${PROJECT_SOURCE_DIR}/Engine/ThirdParty/*.h"
@@ -152,10 +152,6 @@ add_library(WaveEditorSupport STATIC
152152
${WAVE_EDITOR_SHADERS})
153153
add_executable(WaveEditor ${WAVE_EDITOR_ENTRY})
154154

155-
# Temporary build-target compatibility during G0-01 migration. This does not
156-
# create a second executable or a legacy WaveEngine.exe artifact.
157-
add_custom_target(WaveEngine DEPENDS WaveEditor)
158-
159155
set_property(TARGET WaveRuntime PROPERTY COMPILE_WARNING_AS_ERROR ON)
160156
set_property(TARGET WaveEditorSupport PROPERTY COMPILE_WARNING_AS_ERROR ON)
161157
set_property(TARGET WaveEditor PROPERTY COMPILE_WARNING_AS_ERROR ON)
@@ -443,6 +439,22 @@ if(BUILD_TESTING)
443439
LABELS "build;contract;runtime;cpu"
444440
)
445441

442+
add_test(
443+
NAME WaveEngine.EditorWindowLifecycleContracts
444+
COMMAND "${Python3_EXECUTABLE}"
445+
"${PROJECT_SOURCE_DIR}/Tests/editor_window_lifecycle_contracts.py"
446+
"$<TARGET_FILE:WaveEditor>"
447+
)
448+
set_tests_properties(
449+
WaveEngine.EditorWindowLifecycleContracts
450+
PROPERTIES
451+
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
452+
TIMEOUT 120
453+
LABELS "contract;editor;gpu;lifecycle"
454+
ENVIRONMENT "WAVE_RG_STRICT=1"
455+
RUN_SERIAL TRUE
456+
)
457+
446458
add_test(
447459
NAME WaveEngine.McpAssetInputContracts
448460
COMMAND "${Python3_EXECUTABLE}"

Engine/Source/Misc/Log.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class Log
139139

140140
// 选择控制台输出流:
141141
// - 当 stdout 被重定向到 pipe 时(e.g. MCP stdio 模式),走 cerr 避免污染协议流。
142-
// - 否则走 cout(兼容现有 `WaveEngine.exe > log.txt` 的用户)。
142+
// - 否则走 cout(兼容现有 `WaveEditor.exe > log.txt` 的用户)。
143143
// 在静态初始化阶段一次性决定。
144144
static std::ostream& ConsoleStream();
145145

Engine/Source/UI/Panels/ProfilerPanel.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ void FProfilerPanel::DrawQuickActions()
287287
ImGui::EndDisabled();
288288
if (!bViewerAvailable && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
289289
{
290-
ImGui::SetTooltip("WaveTraceViewer.exe was not found beside WaveEngine.exe.");
290+
ImGui::SetTooltip("WaveTraceViewer.exe was not found beside WaveEditor.exe.");
291291
}
292292
}
293293

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
WaveEngine 是面向 Windows x64 的 C++20 实时渲染引擎与 ImGui 场景编辑器。当前唯一渲染后端是 DirectX 12,项目重点是现代 GPU 管线、静态场景编辑、自动化和性能分析,并以 AI-native 作为未来整个引擎的设计约束。
44

55
> [!IMPORTANT]
6-
> 当前版本可用于渲染研究、静态场景搭建和编辑器工具开发,但还不能独立制作与发布完整游戏。Runtime/Editor/Player、Gameplay、物理、音频和打包能力仍在[产品化路线图](docs/plans/2026-07-25-game-engine-production-roadmap.md)
6+
> 当前版本可用于渲染研究、静态场景搭建和编辑器工具开发,但还不能独立制作与发布完整游戏。当前 AI-Native 基础改造只迁移已有能力及其架构边界,不新增 Player、Gameplay、物理、音频或打包功能;长期产品方向见[产品化路线图](docs/plans/2026-07-25-game-engine-production-roadmap.md)
77
88
## 已实现能力
99

@@ -51,14 +51,14 @@ cd WaveEngine
5151
5252
cmake -S . -B Build -G "Visual Studio 17 2022" -A x64
5353
cmake --build Build --config Debug
54-
& .\Build\Debug\WaveEngine.exe
54+
& .\Build\Debug\WaveEditor.exe
5555
```
5656

5757
Release:
5858

5959
```powershell
6060
cmake --build Build --config Release
61-
& .\Build\Release\WaveEngine.exe
61+
& .\Build\Release\WaveEditor.exe
6262
```
6363

6464
`Rebuild.bat` 会复用已有 `Build/` 重新生成并构建 Debug,不是 clean rebuild。构建后 CMake 会复制 Agility SDK、DXC 等运行时 DLL。
@@ -154,7 +154,7 @@ Draft → Accepted → Implementing → Verified
154154

155155
## 当前边界
156156

157-
- 只有单一 `WaveEngine.exe`,没有独立 Runtime、Editor、PlayerGame target
157+
- 已拆分 `WaveRuntime``WaveEditorSupport``WaveEditor.exe`;没有新增 PlayerGame 或发布功能
158158
- 没有完整 World/Entity/Component、Gameplay、PIE、Action Input、物理、音频和 Runtime UI。
159159
- 没有稳定 Asset ID、Cook、Stage 或 Package。
160160
- glTF alpha、双面、UV1、每纹理 sampler 和正确 sRGB 过滤尚未完整实现。

Tests/core_contract_target_contracts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,11 @@ def main() -> int:
179179
)
180180

181181
editor_main = (
182-
repository_root / "Engine" / "Source" / "WaveEngine.cpp"
182+
repository_root / "Engine" / "Source" / "WaveEditor.cpp"
183183
).read_text(encoding="utf-8")
184184
for legacy_fragment in ("CoreRuntimeContracts", "RunCoreRuntimeContracts"):
185185
if legacy_fragment in editor_main:
186-
fail(f"WaveEngine.cpp retains legacy contract runner usage: {legacy_fragment}")
186+
fail(f"WaveEditor.cpp retains legacy contract runner usage: {legacy_fragment}")
187187

188188
print(
189189
"WaveCoreContracts boundary passed: "
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env python3
2+
"""Exercise existing WaveEditor resize/minimize/MCP/normal-exit behavior."""
3+
4+
from __future__ import annotations
5+
6+
import ctypes
7+
from ctypes import wintypes
8+
import json
9+
import os
10+
import queue
11+
import subprocess
12+
import sys
13+
import threading
14+
import time
15+
from pathlib import Path
16+
17+
18+
RESPONSE_TIMEOUT_SECONDS = 30.0
19+
WINDOW_TIMEOUT_SECONDS = 30.0
20+
SW_MINIMIZE = 6
21+
SW_RESTORE = 9
22+
SWP_NOMOVE = 0x0002
23+
SWP_NOZORDER = 0x0004
24+
SWP_NOACTIVATE = 0x0010
25+
26+
27+
def fail(message: str) -> None:
28+
raise RuntimeError(message)
29+
30+
31+
def wait_until(predicate, timeout: float, message: str) -> None:
32+
deadline = time.monotonic() + timeout
33+
while time.monotonic() < deadline:
34+
if predicate():
35+
return
36+
time.sleep(0.05)
37+
fail(message)
38+
39+
40+
class RpcSession:
41+
def __init__(self, process: subprocess.Popen[str]):
42+
self.process = process
43+
self.responses: queue.Queue[str | None] = queue.Queue()
44+
self.stderr_lines: list[str] = []
45+
self.next_id = 0
46+
threading.Thread(target=self._read_stdout, daemon=True).start()
47+
threading.Thread(target=self._read_stderr, daemon=True).start()
48+
49+
def _read_stdout(self) -> None:
50+
assert self.process.stdout is not None
51+
try:
52+
for line in self.process.stdout:
53+
self.responses.put(line)
54+
finally:
55+
self.responses.put(None)
56+
57+
def _read_stderr(self) -> None:
58+
assert self.process.stderr is not None
59+
for line in self.process.stderr:
60+
self.stderr_lines.append(line.rstrip())
61+
62+
def send(self, method: str, params=None):
63+
assert self.process.stdin is not None
64+
self.next_id += 1
65+
request = {"jsonrpc": "2.0", "id": self.next_id, "method": method}
66+
if params is not None:
67+
request["params"] = params
68+
os.write(
69+
self.process.stdin.fileno(),
70+
(json.dumps(request, separators=(",", ":")) + "\n").encode("utf-8"),
71+
)
72+
try:
73+
line = self.responses.get(timeout=RESPONSE_TIMEOUT_SECONDS)
74+
except queue.Empty:
75+
fail(f"timed out waiting for {method}")
76+
if line is None:
77+
fail(f"WaveEditor closed stdout while waiting for {method}")
78+
response = json.loads(line)
79+
if response.get("id") != self.next_id:
80+
fail(f"unexpected response id for {method}: {response!r}")
81+
if "error" in response:
82+
fail(f"{method} returned {response['error']!r}")
83+
return response.get("result")
84+
85+
86+
def find_process_window(user32, process_id: int) -> int | None:
87+
result: list[int] = []
88+
callback_type = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
89+
90+
@callback_type
91+
def visit_window(window, _parameter):
92+
owner_process_id = wintypes.DWORD()
93+
user32.GetWindowThreadProcessId(window, ctypes.byref(owner_process_id))
94+
if owner_process_id.value == process_id and user32.IsWindowVisible(window):
95+
result.append(int(window))
96+
return False
97+
return True
98+
99+
user32.EnumWindows(visit_window, 0)
100+
return result[0] if result else None
101+
102+
103+
def main() -> int:
104+
if len(sys.argv) != 2:
105+
raise SystemExit("expected path to WaveEditor.exe")
106+
if sys.platform != "win32":
107+
raise SystemExit("WaveEditor window lifecycle contracts require Windows")
108+
109+
repository = Path(__file__).resolve().parents[1]
110+
executable = Path(sys.argv[1]).resolve()
111+
if not executable.is_file():
112+
raise SystemExit(f"executable not found: {executable}")
113+
114+
process = subprocess.Popen(
115+
[str(executable)],
116+
cwd=repository,
117+
stdin=subprocess.PIPE,
118+
stdout=subprocess.PIPE,
119+
stderr=subprocess.PIPE,
120+
text=True,
121+
bufsize=1,
122+
)
123+
session = RpcSession(process)
124+
user32 = ctypes.WinDLL("user32", use_last_error=True)
125+
user32.IsIconic.argtypes = [wintypes.HWND]
126+
user32.IsIconic.restype = wintypes.BOOL
127+
user32.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
128+
user32.ShowWindow.restype = wintypes.BOOL
129+
user32.SetWindowPos.argtypes = [
130+
wintypes.HWND,
131+
wintypes.HWND,
132+
ctypes.c_int,
133+
ctypes.c_int,
134+
ctypes.c_int,
135+
ctypes.c_int,
136+
wintypes.UINT,
137+
]
138+
user32.SetWindowPos.restype = wintypes.BOOL
139+
140+
normal_exit_requested = False
141+
try:
142+
methods = session.send("rpc.discover")
143+
if "editor.app.request_exit" not in methods:
144+
fail("rpc.discover omitted editor.app.request_exit")
145+
146+
window_holder: list[int | None] = [None]
147+
148+
def capture_window() -> bool:
149+
window_holder[0] = find_process_window(user32, process.pid)
150+
return window_holder[0] is not None
151+
152+
wait_until(
153+
capture_window,
154+
WINDOW_TIMEOUT_SECONDS,
155+
"timed out waiting for the WaveEditor window",
156+
)
157+
window = wintypes.HWND(window_holder[0])
158+
159+
if not user32.SetWindowPos(
160+
window,
161+
None,
162+
0,
163+
0,
164+
1200,
165+
720,
166+
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE,
167+
):
168+
fail(f"SetWindowPos failed with Win32 error {ctypes.get_last_error()}")
169+
time.sleep(0.25)
170+
frame_stats = session.send("editor.profile.frame_stats")
171+
if not isinstance(frame_stats, dict):
172+
fail("editor.profile.frame_stats did not return an object after resize")
173+
174+
user32.ShowWindow(window, SW_MINIMIZE)
175+
wait_until(
176+
lambda: bool(user32.IsIconic(window)),
177+
WINDOW_TIMEOUT_SECONDS,
178+
"WaveEditor did not enter minimized state",
179+
)
180+
hierarchy = session.send("editor.hierarchy.list")
181+
if not isinstance(hierarchy, list):
182+
fail("editor.hierarchy.list did not return an array while minimized")
183+
if hierarchy:
184+
session.send("editor.hierarchy.select", [hierarchy[0]["id"]])
185+
186+
user32.ShowWindow(window, SW_RESTORE)
187+
wait_until(
188+
lambda: not bool(user32.IsIconic(window)),
189+
WINDOW_TIMEOUT_SECONDS,
190+
"WaveEditor did not restore from minimized state",
191+
)
192+
session.send("editor.profile.frame_stats")
193+
194+
user32.ShowWindow(window, SW_MINIMIZE)
195+
wait_until(
196+
lambda: bool(user32.IsIconic(window)),
197+
WINDOW_TIMEOUT_SECONDS,
198+
"WaveEditor did not enter the second minimized state",
199+
)
200+
session.send("editor.app.request_exit")
201+
normal_exit_requested = True
202+
process.wait(timeout=RESPONSE_TIMEOUT_SECONDS)
203+
if process.returncode != 0:
204+
fail(f"WaveEditor returned {process.returncode} after normal request_exit")
205+
finally:
206+
if process.poll() is None:
207+
if not normal_exit_requested:
208+
try:
209+
session.send("editor.app.request_exit")
210+
except (BrokenPipeError, OSError, RuntimeError):
211+
pass
212+
try:
213+
process.wait(timeout=5.0)
214+
except subprocess.TimeoutExpired:
215+
process.terminate()
216+
try:
217+
process.wait(timeout=5.0)
218+
except subprocess.TimeoutExpired:
219+
process.kill()
220+
process.wait(timeout=5.0)
221+
222+
print(
223+
"WaveEditor window lifecycle contracts passed: resize, minimize, "
224+
"main-thread MCP drain, restore, and minimized request_exit"
225+
)
226+
return 0
227+
228+
229+
if __name__ == "__main__":
230+
try:
231+
raise SystemExit(main())
232+
except (OSError, RuntimeError, subprocess.SubprocessError) as error:
233+
print(f"WaveEditor window lifecycle contracts failed: {error}", file=sys.stderr)
234+
raise SystemExit(1)

Tests/mcp_asset_input_contracts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"""Focused contracts for MCP framing, Editor selection, and malformed assets.
33
44
Usage:
5-
py -3 Tests/mcp_asset_input_contracts.py Build/Debug/WaveEngine.exe
5+
py -3 Tests/mcp_asset_input_contracts.py Build/Debug/WaveEditor.exe
66
77
The test only invokes operations expected to fail plus transient Editor
88
selection, so it does not alter the saved scene. It asks the child process to
@@ -34,7 +34,7 @@ def _reader(stream, output: queue.Queue[str | None]) -> None:
3434

3535
def main() -> int:
3636
if len(sys.argv) != 2:
37-
raise SystemExit("expected path to WaveEngine.exe")
37+
raise SystemExit("expected path to WaveEditor.exe")
3838

3939
repository = Path(__file__).resolve().parents[1]
4040
executable = Path(sys.argv[1]).resolve()

0 commit comments

Comments
 (0)