目前的狀態
已经不像 RenderDoc 的:
• CTWResearcher.dll / .exe
• 导出 CTW_*、CTW_App_GetAPI(不再是 RENDERDOC_GetAPI)
• 版本资源 ProductName
• Vulkan 层名 VK_LAYER_CTWRESEARCHER_Capture
• 全局 hook 的 file mapping 名
进了目标进程之后,Windows 和模块内容里仍然是 RenderDoc 的,按对方容易拿到的顺序:
1. DLL 映像里的明文
LdrLoadDll 钩子不只看路径。它可以在放行前读文件或映射后扫 .rdata。你这份 DLL 里还大量留着:
• C++ RTTI:class RenderDoc → 二进制里是 .?AVRenderDoc@@
• 路径字面量 %TEMP%\RenderDoc\*.rdc / *.log
• renderdoc.conf
• 注册表 RenderDoc.RDCCapture.1
• 源码路径、断言、__FILE__,以及工程目录名 renderdoc-1.x
• PDB 路径,只要 PE debug 目录没剥干净
文件名已经叫 CTWResearcher.dll,内容扫描照样命中。这是「改名了却仍失败」最常见的原因,也不需要扫全进程内存。
2. 进程在 Windows 里留下的活对象(DllMain 里立刻就会做)
注入成功后 RenderDoc::Initialise() 会:
• 在 38920–38927 上 listen。这是 RenderDoc 的固定控制端口。目標在本进程里看 TCP 表,或自己先占 38920,几乎零误报。
• 在 %TEMP%\RenderDoc\ 下建目录、写 log/rdc。
• Present 上画 "Capturing D3D11" / "F12, PrtScrn to capture"。这是公开的 UI 指纹,查 overlay 或挂钩后的交换链就能认。
• 挂钩 DXGI/D3D 虚表。不查名字也能发现 Present 被换掉。
这些都是「进程、DLL、Windows 互相说话」时漏出去的,不是文件名。
3. 你自己这条链路里,也还在用旧名字找对方
即使游戏放行了,CTW 内部还有几处在认 RenderDoc 的壳:
• 找 UI:RenderDoc.RDCCapture.1\DefaultIcon
• 捕获/日志目录:RenderDoc\
• 配置:renderdoc.conf
• renderdoccmd 窗口类
• crash dump 目录名 RenderDoc
表现为:注入器以为成功了,目标控制连不上、F12 拉不起 UI、捕获文件写到另一边在找的目录。看起来也像「没成功」,但锅在自己改名改一半。
之前的改名只改了“表层”——PE 导出符号名、文件名、Replay Marker、Vulkan layer。但没改“内容”——字符串字面量、shader 里的函数名、日志文案、路径、__FILE__源码路径。
扫描当前 CTWResearcher.dll,明文残留是: renderdoc 601 次 renderdoc-1.x 277 次 <- 源码路径 __FILE__ RENDERDOC 139 次 RenderDoc 90 次 样本分类很清楚: Shader 源码函数名: RENDERDOC_PixelHistoryCopyPixel RENDERDOC_QuadOverdrawPS RENDERDOC_MeshGS / TriangleSizeGS / HistogramCS ... 日志/断言字符串: !!!!RenderDoc Internal: Replay %d !RenderDoc::Inst().IsReplayApp() 源码绝对路径: E:\college_researcher\renderdoc-1.x\renderdoc\driver\d3d11\d3d11_resources.h 路径字面量: /RenderDoc/RemoteServer_Client.log /RenderDoc/RemoteServer_Server.log /files/renderdoc.conf /renderdoc_report_%H%M%S.zip /../share/renderdoc/plugins /libVkLayer_GLES_RenderDoc.so目标进程的LdrLoadDll钩子不用看文件名,映射后扫一下.rdata,甚至直接读文件搜"renderdoc"这个子串,601 处命中,直接拒绝。这就解释了“文件已经叫CTWResearcher.dll,却还是失败”。不是 hook 拦了什么高深东西,是内容一眼就是 RenderDoc。
3. 控制端口还是原版的 38920 / 39920。这不是字符串 renderdoc,但有人按「是不是 RenderDoc」去扫端口时仍对得上。
4. 版本信息里 CompanyName 仍是 Baldur Karlsson。普通模块名扫描不会碰这个;看文件属性的人能看到。
注意EXE 和 DLL 不是同一批编出来的,导入名对不上。這個情況,編了一些另一些沒編,
episode
在日常英語中,episode指的是一個有起承轉合、相對獨立的事件或時期。它不一定跟電視有關:
- 情緒/病情發作:醫生會說 "a depressive episode"(抑鬱症狀發作期)或 "a psychotic episode"。這指的不是「一集電視」,而是生活中「某一段特定發作的時期」。
- 人生的一段插曲:如果你跟前任有一段荒謬的分手故事,你可以說:"That was a crazyepisodein my life."(那是我人生中一段瘋狂的小插曲)。
為什麼電視要用episode?
電視影集之所以用episode,也是延伸了這個「插曲/片段」的概念。
- 影集中的每一集,就像是主角漫長人生故事(Series/Show)中,某個特定發生的小事件、小插曲。
- 每一集通常有自己獨立的事件和結局,拼湊起來才是完整的人生。
補充:其他中文翻成「集」的英文字
- Episode:強調大故事中的「某個事件/插曲」(如:美劇的一集)。
- Volume (Vol.):通常指書本的「第幾卷/第幾冊」,動漫或輕小說常用。
- Issue:指期刊、雜誌、漫畫的「第幾期」。
git 里 deleted + untracked 是目录重命名,不是删功能
git add -A中的-A代表All(全部)。.txt / .patch 是刚用 git diff --cached 写到项目目录里的拷贝。
在信息和 git diff --cached 完全相同这个条件下,加了后缀的三个都没用。
完整信息只有 03-full.patch。
• --stat:只剩每个文件改了几行,hunk 没了
• --name-status:只剩 M/R/A 和路径,更少
• core:文件集合都变了,信息既不全也不一样它们都是另一份摘要或切片,不能代替 full。要同等信息量,看那一份默认补丁就够。
NVIDIA 的利潤率極高(毛利率長期在 70% 以上),等於把大量利潤從 OpenAI、xAI、Google、Meta 這些公司口袋裡拿走。
AI 公司現在最燒錢的就是算力,推理成本尤其是長期營運的關鍵。如果能自己做芯片,把這塊成本壓下來,對利潤和定價權影響巨大。
.rdc 能抓到。 抓的是那一帧 GPU 实际跑过的东西:VS 字节码、常量缓冲(时间、风速)、顶点缓冲、这次 draw。Mesh Viewer 里一般能看两层:
• VS Input:模型原顶点,通常不带风
• VS Output:这一帧被 VS 吹过之后的位置
所以能确认「是不是 VS 在动、用了哪些参数」,不是只能看到静模型。
限制也清楚:一份 rdc 是一帧快照,草会停在被抓住的那个姿势,不会在回放里自己持续晃。时间变量冻在当时 CB 里。要看运动过程,得连抓几帧,或者对着 VS 和那些时间参数看。
注入失败发生在 DLL 跑起来之前的话,DLL 里还剩什么字符串都还没轮到。全替换清的是映射之后才能扫到的明文;成品这边拦的是加载动作,和 renderdoc 扫没扫到不是同一层。
.symfix+ C:\symbols:設定微軟公開符號伺服器的下載路徑,並將下載的.pdb除錯檔存在C:\symbols。這能讓 WinDbg 把記憶體中的機器碼位址(如0x7FFA1234)翻譯成你認識的函式名稱(如LdrLoadDll)。
.reload:強制偵錯器依據剛剛設定的路徑,重新載入該行程所有模組(.exe / .dll)的符號。
通过启动器勾选“DX11”--1-16,本质上是在修改启动器的配置文件或传递启动参数-2。这个设置一旦保存,就会被记录在启动器的配置里-2。
多游戏引擎(如Unity、UE4/UE5)支持通过命令行参数指定图形API。例如,为游戏EXE的启动参数添加-dx11或-force-d3d11
python rename_branding.py .加--dry-run可以先只看计划改动,不实际写文件。
覆盖范围
它从根目录递归遍历整个仓库,但有三类例外:
- 跳过
.git、.hg、.svn、__pycache__ - 只处理文本文件,
.dll/.exe/.ico/.png等二进制不动 - 不处理脚本自身
它会同时改:
- 文件内容里的
renderdoc - 文件名里的
renderdoc - 目录名里的
renderdoc
另外它的覆盖:只替"renderdoc"
- 不处理
baldurk、Crytek、github.com/baldurk - 不处理图标资源
- 不处理已编译的 Release 二进制
“换图标”本身不需要重新编译 C++ 源码,只需要重新生成资源并重新链接对应的 EXE。
流程是这样的:
- 图标文件
icon.ico被.rc引用 - 改动图标后,
rc.exe把.rc编译成.res - linker 把
.res链接进 EXE
文件同时被 GUI 和 Cmd 的.rc引用:
替换后只重编这两个输出:
msbuild qrenderdoc\qrenderdoc_local.vcxproj /p:Configuration=Release /p:Platform=x64 ... msbuild renderdoccmd\renderdoccmd.vcxproj /p:Configuration=Release /p:Platform=x64 ...CTWResearcher.dll不需要重编,因为图标没嵌在核心 DLL 的.rc里。
60×60 做 128/256 会被放大,高 DPI 下可能不够清晰。如果只是先验证注入,16/32/48 就够;如果要正式报告,最好再提供一张 512×512 的图,生成更干净的 ICO。
把 temp 里的RenderDoc目录名改成了xxxxxx,不是“不写 log”。
能找到并加载 PDB 是最幸福的事——它能极大缩短你理解某段 Shader 或某个 DrawCall 在干什么的时间。
问题不是「这两种写法会不会让 CreateProcess 失败」,而是「这两种写法会不会让第 2 步的 LoadLibraryW 根本没被跑到」。
LaunchAndInjectIntoProcess 实际顺序是:
1. CreateProcess(..., CREATE_SUSPENDED) — 进程已经建好,主线程还没跑
2. InjectDLL — 在这个冻结进程里触发 LoadLibraryW(capture.dll)
3. FindRemoteDLL — 看模块列表里有没有这份 DLL
4. 调 CTW_Implant_* 配路径/选项
5. ResumeThread — 游戏这才开始跑
UI 上的 Failed to inject ...dll 出在第 3 步。CreateProcess 已经成功了,exe 作为普通程序是能建起来的。失败的是:capture DLL 没进目标进程。
RenderDoc 1.44 → 1.46的差距。
2. 注入 DLL 的方式不一样,这是最大嫌疑
CTW 当前在 [win32_process.cpp (line 252)](E:/college_researcher/ctwresearcher-1.x/ctwresearcher/os/win32/win32_process.cpp:252) 里:
VirtualAllocEx分配PAGE_EXECUTE_READWRITE- 写入一段 54 字节自定义 x64 shellcode
CreateRemoteThread的入口点是这段 shellcode- shellcode 再调
LoadLibraryW和GetLastError
Duck 在它的 [win32_process.cpp (line 252)](E:/college_researcher/RenderDuck-1.4-source/RenderDuck-1.4/riderduck/os/win32/win32_process.cpp:252) 里:
- 直接
VirtualAllocEx放 DLL 路径 CreateRemoteThread的入口点直接就是kernel32!LoadLibraryW- 不写自定义 shellcode,也不回读结果
对目标进程来说,这是一个很明显的可区分点:
- Duck 的远程线程入口位于已加载模块
kernel32.dll - CTW 1.46 的远程线程入口位于一段自定义可执行内存
如果目标按“线程起始地址是否属于已知模块”做校验,CTW 的shellcode 注入会比 Duck 更显眼。
3. Duck 还多改了 hook 层
Duck 的 [win32_hook.cpp (line 52)](E:/college_researcher/RenderDuck-1.4-source/RenderDuck-1.4/riderduck/os/win32/win32_hook.cpp:52) 里额外加了:
ApplyExportDetour- 用 Detours 对 D3D/DXGI 导出函数做 export detour
- 额外 hook
kernelbase.dll - 不只 hook
kernel32.dll
这些不是“品牌改名”,是实际行为差异。CTW 目前没有这部分。
VirtualAllocEx 分配 PAGE_EXECUTE_READWRITE 内存,是远程代码注入和恶意软件分析中一个非常关键且敏感的操作。它意味着在目标进程中分配一块可执行、可读、可写的内存区域。
GPA MCP,是作者为了将Intel GPA这个强大的图形分析工具接入 AI 工作流(如 Codex)而开发的一个 MCP 服务
.gpa_frame
把 AI 的请求转交给一个已经打开并加载了该文件的 RenderDoc 进程(qrenderdoc.exe)
按你设的条件(必须能断定是 RenderDoc,还不能误伤别的软件),这段 54 字节 stub 没有机制上的排他身份。它不是一份能枚举到的 DLL,模块列表里也没有它。
这段 stub 目标进程实际能看到什么
它不是模块,只是一块匿名内存加一个远程线程:
• 起始 RIP 不在任何已加载映像里
• 那块内存是新分配的 PAGE_EXECUTE_READWRITE
• 内容是代码,不是路径字符串
• 跑完就被 VirtualFreeEx 掉,连常驻模块都不是
在「不能误伤」这个约束下,这些都不够。CreateRemoteThread、远程 RWX、线程入口不在模块里,是一整类加载器都会留下的痕迹,overlay、调试器、别的注入器都有。拿这个当 RenderDoc 检测,误伤面太大。
指令模板本身也不构成「这就是 RenderDoc」:
• 中间 16 字节是运行时填进去的 LoadLibraryW / GetLastError 地址,开机 ASLR 一变,整段 54 字节就不能当固定哈希
• 能稳住的只有骨架:sub rsp,0x28、两次 call rax、往 [rbx+0x208] / [rbx+0x210] 回写
• 0x208 只是 MAX_PATH * sizeof(wchar_t),这是很普通的路径缓冲区布局
• 这段 stub 官方 RenderDoc 和 Duck 都没有。按 1.44/1.45/Duck 写的检测器,本来就不会找它
所以:它最多能被认成「有人用了一个带错误回写的 LoadLibrary 小助手」,不能排他地认成 RenderDoc。你说的对——除非目标已经在专门对着 CTW 这份实现做字节级特征,而这正好违反「不能误伤、且必须能断定是 RenderDoc」。
「一段手写的机器码字节,写进别人进程里直接当入口执行」,不是编译进某个 DLL 里的函数。
最早这个词指用来弹出 shell 的载荷。后来泛化了:只要是这种不落在模块里的裸指令块,都还叫 shellcode。CTW 这份 54 字节 stub 就是这个形态,所以代码里也写成 shellcode[]。它并不弹 shell。
git commit -a(或 git commit --all)默认不会添加新文件(即未被 Git 跟踪的文件),它只会自动暂存已被跟踪文件的修改和删除。
detour 就是 绕道:在真正的函数开头插一条跳转,调用先走到你的函数,你再决定要不要去执行原来的。
Duck 用的是微软的 Detours 库。ApplyExportDetour 不是改 IAT 里的指针,而是直接改 d3d11!D3D11CreateDevice 这类导出函数本身的前几个字节,改成 jmp 到 hook。官方 RenderDoc / CTW 只改导入表,不改函数本体。
IAT 在计算机领域通常指导入地址表(Import Address Table),是 Windows 可执行文件(PE 格式)中的一个核心数据结构。
官方 RenderDoc 用 hook GetProcAddress、新模块再扫一遍 IAT,挡住常见的「查地址再调」。Duck 的 detour 更进一步:把 d3d11!D3D11CreateDevice 函数开头改成跳到 hook,只要跳进这个导出,不管从哪来都会中。
游戏平时这样调:
游戏 --读自己的 IAT--> 跳到 d3d11 的函数
IAT hook 改的是调用方的那一格,不是 d3d11。
capture DLL 把游戏 IAT 里那个指针改成自己的 hook:
游戏 IAT 某一格 ──► capture 的 hook ──► 再转去真的 D3D11CreateDevice
d3d11.dll 里一个字节都没动。游戏也没被通知,它还以为自己在调 D3D。
renderdoc官方还 hook 了 GetProcAddress:目標進程調用GetProcAddress,會得到被hook 返回的地址
拦不住的情况——自己解析导出表或提前缓存
官方 RenderDoc 原本怎么做
官方/CTW 原来的win32_hook.cpp只做两件事:
- 在模块加载时,把模块 IAT 里的
LoadLibrary*、GetProcAddress、D3D/DXGI 函数指针替换成 hook - 之后靠 hook 住的
GetProcAddress,对以后才解析出来的函数也返回 hook
这个方式只能覆盖:
- 通过 IAT 调用的路径
- 通过被 hook 的
GetProcAddress拿到的函数指针
但如果目标程序早就拿到了Present、SwapChain、CommandQueue等真实导出地址,或者它自己直接调用dxgi.dll!Present的导出入口,不走 IAT,那 IAT patch 就抓不到。
Duck 加的ApplyExportDetour做了什么
Duck 在 [win32_hook.cpp (line 90)](E:/college_researcher/RenderDuck-1.4-source/RenderDuck-1.4/riderduck/os/win32/win32_hook.cpp:90) 里加了 Detours 的 export detour:
GetProcAddress(module, function) -> DetourAttach(&original, hook)它会直接改写目标模块真实导出函数入口的前几条指令,让任何调用方:
- 通过 IAT 调用
- 通过
GetProcAddress拿到指针后调用 - 自己缓存了函数指针再调用
- 内部模块直接 call export 地址
都会落到 RenderDoc 的 hook 上。
kernel32.dll 是“壳”:从 Windows 7 开始,微软为了优化系统架构和向后兼容性,将大部分核心 API 的实际实现代码移到了 kernelbase.dll 中
只 Hook kernel32.dll 的局限:如果你只 hook 了 kernel32.dll 中的 LoadLibraryW 或 GetProcAddress,那么当程序直接调用 kernelbase.dll 中的实现时,你的 hook 就不会被触发。因为调用根本没有经过 kernel32.dll 的“跳板”。
kernel32.dll 中的函数(如 WriteFile)在被调用时,会立即跳转(转发)到 kernelbase.dll中对应的函数去执行
API Set 机制:现代 Windows 还引入了“API Set”机制,像 api-ms-win-core-libraryloader-l1-1-0.dll 这样的文件是虚拟的、不包含实际代码的“占位符”。Windows 加载器会根据内部的 Schema 规则,将这些 API Set 调用重定向到真正的实现模块(如 kernelbase.dll)
https://www.zhihu.com/question/4494136057/answer/66932168380#:~:text=%E8%BF%99%E4%BA%9B%E6%96%87%E4%BB%B6%E8%83%BD,PE%E7%BB%93%E6%9E%84%E3%80%82http://Windows API set stub dll 文件存在的作用是什么?
虽然是合法的 PE 格式 DLL,但内部几乎是空的,只包含一个导出表(相当于门牌号)和最基本的 PE 结构,没有任何实际的函数实现代码。
它在哪些时机执行
Duck 在四处都调ApplyExportDetour:
- 模块第一次被
ApplyHooks处理时 - 模块有多个副本,另一个副本变成主模块时
Hooked_GetProcAddress发现模块并初始化 hook 时Win32_ManualHookModule手动 hook 时
并且在RemoveHooks里用DetourDetach清理,避免卸载时留下被改写的导出函数。
你打开 qriderduck.exe:DLL 进 UI 进程,DllMain 发现 replay 标记,按回放程序初始化。游戏还没启动。
2. 你点 Launch:UI 进程里调用 InjectDLL。
3. LoadLibraryW 在 游戏进程 里成功:DLL 再进游戏,DllMain 再跑一次,这次没有 replay 标记,才注册 hook。
UI 进程里下一步就是 WaitForSingleObject(hThread, INFINITE),等到那条远程线程结束。
那条线程本身就是 LoadLibraryW。它要先完成映射,再跑完目标里的 DllMain,才会返回。等的就是这件事结束,不是另起一个查询。
从 Visual Studio 2015 开始,MSVC 工具集主版本号统一为 14,v140、v141、v142、v143 之间基本保持二进制兼容。这意味着你可以混合使用不同版本工具集生成的二进制文件,但必须使用最新的工具集至少与应用中的最新二进制链接
- name mangling 规则变化:v143 为支持 C++20 模块,将 std::allocator 的实例化细节更深地嵌入 mangled name,导致即使源码完全相同,v143 生成的符号也无法被 v142 链接器识别。
STL 布局差异:某些情况下,Debug 版 std::vector 的成员布局在 v142 和 v143 之间可能产生偏移差,触发 _ITERATOR_DEBUG_LEVEL mismatch 断言。
Cocos Creator 打 Windows 包:使用 Cocos Creator 2.4.3 打包 Windows 游戏时,需要 Visual Studio 2019 并选择MSVC v142和Windows 10 SDK (10.0.16299.0)
UE4.27 兼容:如果你使用 VS2022 开发 UE4.27,需要额外勾选MSVC v142工具集,因为 UE4.27 依赖 v142 工具链。
CTW 1.46 使用 MSVC v143 时,DLL 静态导入表包含额外 CRT/Win32 函数,与目标可接受的已测版本不一致,导致加载器在 DllMain 前拒绝;改用 v142 后,导入表与可行版本完全一致,注入成功。
C:\Users\86134\AppData\Local\Temp\CTWResearcher\这个路径写在win32_stringio.cpp的GetDefaultFiles()里:
GetTempPathW() + "CTWResearcher\\..."默认捕获文件会类似:
C:\Users\86134\AppData\Local\Temp\CTWResearcher\CTWResearcher_app_2026.08.26_20.xx.rdc日志也在同一目录:
C:\Users\86134\AppData\Local\Temp\CTWResearcher\CTWResearcher_app_*.log某些游戏会根据硬件或驱动检测,在启动时自动选择最佳 API,但同样不会中途更换。
为什么能获取到游戏进程的命令行
不是遊戲在「即時隨便換圖形 API」,而是 RenderDoc 在多個 API / swapchain 同時存在時,用 F11 切換「目前要捕捉哪一個」。
也就是:
- 一個 process 裡可能同時有多個 graphics API 在跑(最常見的例子是 Chrome / 瀏覽器:D3D11 + D3D12 同時 active,WebGPU 走 D3D12,其他東西可能走 D3D11)。
- 也可能有多個 swapchain / window(多視窗、多 monitor、或引擎內部有額外的 offscreen / overlay 視窗)。
- RenderDoc 同一時間只能把「目前 active 的那一個」當作捕捉目標。
- 按F11就是在這些已存在的 API / window / swapchain 之間循環切換「誰是目前 active」。
所以 overlay 上你會看到類似 D3D11 (Active), D3D12 (Active) 這種狀態,按 F11 可以把 Active 標記從一個移到另一個。
「显示更多选项」,或 Shift+右键
靠,現在才知道還有這樣的shift右鍵,
因为安装时没勾「把 Open with Code 加到资源管理器右键」。这是 User 安装,Code.exe 本身在,但注册表里原来没有这项。
已经补上了。文件、文件夹、文件夹空白处都能右键 Open with Code。Win11 新右键可能还是要先点「显示更多选项」,或 Shift+右键。关一次资源管理器窗口再试。
Unity 这层被 force 成 D3D11(一般是 -force-d3d11,或 Player 只开了 D3D11)
• 真正出画面的是后面那套自定义 Vulkan(log 里的 [Vulkan init] / HGRP)
• boot.config 里没有 API 开关,只有 gfx-enable-native-gfx-jobs 和远程 engine_config
• 注册表 HKCU\Software\Hypergryph\Endfield 只有画质(DLSS、framegen、阴影),没有 DX12/Vulkan 选项
• 远程 engine_config 里也没有图形 API 字段
所以你在 CTW 里只能跟 Vulkan,不是配置没读到,是 present 的就是 Vulkan。你之前那份 capture log 也写了 Used API: Vulkan (Presenting)。中间冒出来的 D3D12 是短命设备(然后被 Remove),不是游戏主渲染。
Command-line Arguments / Environment Variables 能干涉,这是你这边能塞参数的地方。框是空的,CTW 就按光 exe 启动,游戏自己走 Player.log 里那套:Unity 层 force D3D11,出画面的还是 Vulkan。
如果你在 Arguments 里填 -force-d3d12,最多动 Unity 那层 GfxDevice,动不了后面的 Vulkan 呈现。
CTW 点Launch 时走的是 Windows 的 CreateProcess:由它指定 exe 路径、工作目录、环境变量、以及整条命令行。操作系统原样交给新进程。资源管理器双击等于命令行只有 exe 自己;你在这个框里加字,只是让 CTW 多传几个 token。这是创建进程的常规能力,不是 CTW 特供。
游戏是 Unity,常见的 Unity 图形后端参数:
-force-d3d11 -force-d3d12 -force-vulkan -force-glcore -force-metal -force-feature-level-11-0 -force-feature-level-11-1其他 Unity 常用参数:
-screen-width 1920 -screen-height 1080 -screen-fullscreen 0 -popupwindow -window-mode borderless -window-mode exclusive -window-mode fullscreen -window-mode windowed -vsync 1 -no-vsync -logFile C:\path\game.log -disable-gpu-skinning -nographics -batchmode如果是 UE 游戏,则是另一套:
-d3d12 -dx12 -vulkan -sm5 -sm6 -rhiapi=d3d12 -rhi=vulkan所以不能给“所有 EXE 默认加-force-d3d12”。正确做法是给每个游戏建一个启动预设,里面写它认识的参数:
启动时实际选了什么,用 Unity 自己的 log:
C:\Users\86134\AppData\LocalLow\Gryphline\Endfield\Player.log
E:\GRYPHLINK\games\Arknights Endfield\Endfield_Data\boot.config
Established + D3D12 Not Presenting + 游戏里能截帧说明你连接的 LiveCapture 不是真正 presenting/capturing 的那个进程。
这个游戏不是单进程:CTW 会注入多个子进程。你现在连接的进程:
- 目标控制连接 Established
- 但它没有 presenting 的 D3D12 窗口
- 所以 API 显示 Not Presenting
实际截帧发生在另一个子进程里。那个子进程有自己的:
- target control ident
- API 状态
- capture list
Collected Captures 只收集当前 LiveCapture 所连接的那个进程的帧。如果帧发生在子进程,而子进程的 LiveCapture 没有打开,主连接里就看不到。
源码里对应的地方:
LiveCapture::ConnectToChild() LiveCapture::NewChild子进程出现时,LiveCapture 会添加 Child 条目。你应该在 LiveCapture 窗口里选那个真正 presenting 的子进程,打开它的连接:
- 它的 API 应显示
D3D12 (Presenting & supported) - 它的 Collected Captures 里才会出现这次 F12
CC Switch 里 通用配置(common_config_codex)是 Codex 全局的。你新加一个供应商/号,切换过去时会拼成:
该供应商自己的 config(模型、model_providers、key)
+ 通用配置(personality、[windows]、项目 trust)
→ 写入同一份 C:\Users\86134\.codex\config.toml
同一份工程、同一路径下,没改过的工程 MSBuild 会跳过。 这次没有跳过,是因为目录从 ctwresearcher-1.44 挪到了 ctwresearcher-1.44-source,旧的增量记录对不上,等于全量重来。大约 6 分钟,exit code 0。
Windows 官方做法是直接開 renderdoc.sln 編,不用 CMake(root CMakeLists.txt 裡甚至寫了 Windows 不需要 CMake)。
solution 裡是多個獨立 VS project,常見輸出大致是:
| Project | 典型產物 | 角色 |
|---|---|---|
| renderdoc | renderdoc.dll | 核心 capture / replay runtime |
| qrenderdoc | qrenderdoc.exe | Qt UI |
| renderdoccmd | renderdoccmd.exe | CLI |
| renderdocshim 等 | shim DLL | injection / hook |
| d3d12 等 driver 相關 project | 對應 DLL 或靜態庫 | API backend |
| pyrenderdoc_module / qrenderdoc_module | .pyd | Python bindings |
| version、IHV plugin(例如 NV) | stub / plugin | 輔助 |
MSBuild 不是「一次只能編一個」
差別在你餵給 MSBuild 的是整個 sln還是單一 vcxproj。
- 一個 vcxproj ≈ 一個 DLL/EXE:這層你的理解是對的。
- 一次 msbuild xxx.sln 可以編很多個:MSBuild 會依 project 相依順序排程,/m 還能平行編不同 project。
- 不是「編譯器一次只能產出一個檔案」,而是「一個 project 定義一個 output;solution 把很多 project 串在一起」。
9 个字符,现在 8 个字符加一个结束符,后面的数据没有挪动:
改前: Chart.dll \0 \0 \0 paus
改后: Chat.dll \0 \0 \0 \0 paus
启动器里已经搜不到 Chart.dll,只剩一处 Chat.dll。直接跑 Launcher.exe 即可,它会去加载旁边的 Chat.dll。
Peeking多半不是 Cheat Engine 那種「讀記憶體改血量」,而是注入 DLL 的選單裡一個跟畫面遮擋 / 淡出有關的視覺開關。你前面在看 Endfield,這類遊戲(星鐵、原神、絕區零、明日方舟:終末地)的 trainer 很常有同名功能。
它實際在改什麼
這類遊戲相機會貼牆、鑽進模型裡。為了不穿模難看,引擎會把角色或前景dither / fade out(半透明、點狀消失)。函式名通常長這樣:
- SetElevationDitherAlphaValue
- SetDitherAlphaValueWithAnimation
- elevation / camera occlusion fade
Peeking ON= hook 這些函式,把 fade alpha 強制設成 1,或直接 return 不跑淡出。
結果是視覺上的:
- 鏡頭貼牆時角色不再「溶掉」
- 被地形挡住的淡出變弱,看起來比較能「往裡看一眼」
- 有些實作會連帶讓遮擋物透明度異常,接近輕量透視
所以才說它和 visual 有關:改的是 shader / material 的 alpha、dither 參數,不是 HP、金幣那種 gameplay 數值。選單裡常跟 ESP、speedhack 放一起,但原理不同。
和「DLL 改數值」差在哪
| 改數值(peek/poke) | Peeking(視覺) | |
|---|---|---|
| 對象 | 記憶體裡的 int/float(血、錢、冷卻) | 渲染參數(dither alpha、occlusion) |
| 手段 | Read/WriteProcessMemory 或直接寫物件欄位 | hook 渲染 / shader property 函式 |
| 你看得到的變化 | UI 數字變了 | 畫面不再淡出、遮擋變少 |
| 成敗 | 加密、校驗、伺服器權威會擋 | 純客戶端畫面,單機/單人場景特別明顯 |
老術語裡peek = 讀、poke = 寫。現代 ImGui 外掛把「關鏡頭淡出」也叫 Peeking,名字容易混。
FPS 的peeker's advantage:網路延遲造成「先探頭的人先看到」,跟 DLL 無關
API 常見是vertical FOV(Unity、D3D 慣例),也有 horizontal FOV。另加一個aspect決定左右有多寬。
多數引擎寫的是vertical FOV,左右張角由 aspect 推: f o v h = 2 arctan ( tan ( f o v v / 2 ) ⋅ a s p e c t ) \mathrm{fov}_h = 2\arctan(\tan(\mathrm{fov}_v/2)\cdot\mathrm{aspect}) 。寬螢幕看起來更「廣」,不一定是又改了一次 FOV。
遊戲裡「FOV 滑條」有時只改 world camera,槍/UI 用另一套 projection,所以準星和場景拉伸不一致。
極少數會在改 FOV 時順便移相機或改 near,那是他們自己的鏡頭預設,不是 FOV 定義的一部分。
水平張角本身不能寫成 α h = α v × 16 / 9 \alpha_h = \alpha_v\times 16/9 。正確是半角走 tangent:
60° vertical 在 16:9 上大約是 90.6° horizontal,不是 60 × 16 / 9 = 106.7 ° 60\times16/9=106.7° 。大角度時誤差很明顯。
為什麼這套常被當成預設
gluPerspective(fovy, aspect, n, f)、D3D、Unity 都是vertical FOV + width/height。好處是改解析度、超寬屏時:
- 上下看到的範圍鎖定(角色、準星垂直位置穩)
- 多出來的像素往左右加(Hor+),而不是把上下裁掉
為什麼一定是半角
視錐左右、上下是對光軸對稱的。從相機看,你真正要的是「中心到邊緣」這一條邊的斜率:
近平面全高是 2 h 2h ,所以全角定義是「兩邊各一箇半角」:
公式裡的 2 只是把上下(或左右)兩半拼回一個張角。推寬高、組投影矩陣時,用的一直是 tan ( α / 2 ) \tan(\alpha/2) ,不是 tan ( α ) \tan(\alpha) 。
丢掉半角會怎樣
tan \tan 不是線性的:
如果硬用全角,近平面半高會變成 n tan α n\tan\alpha ,那是把整個張角都堆到光軸同一側,視錐直接歪掉,而且 90° 就炸。
要從 α v \alpha_v 推 α h \alpha_h ,中間也必須進半角再出來,沒有「 α v × a s p e c t \alpha_v\times\mathrm{aspect} 」這種捷徑。
不是「半角不方便、最後還要把 2 補回來」;而是度數是給人看的,
投影必須把整個視錐填進同一塊 clip / 螢幕矩形。FOV 只改側邊斜率,近平面作為成像窗口還是那一塊像素,所以多出來的世界只能被縮小後塞進去。
极好的可视化网站
Frustum
WebGL Visualizing the Camera
three.js examples
資訊量上可以收成這樣:
無量綱的全是形狀,不能單獨定出世界裡的那顆錐。要落地,必須錨上一個帶長度量綱的量。
FOV、aspect、 tan ( α / 2 ) \tan(\alpha/2) 、甚至 f / n f/n ,都是比值或角度,縮放世界它們不變,畫面看起來一樣。它們是條件熵裡「形」的那部分,只能當擴展。
把這套比例嵌進 R 3 \mathbb{R}^3 、讓「17」不再只是純數字,需要一個長度。圖形管線裡這個長度被放在:
图里采用的是经典 OpenGL 风格的[-1, +1]:
Near → -1 Far → +1这不是 UE 当前实际使用的方向
Near Clip Plane 太小会严重损害 depth precision。
假设场景真正关心的是:
5 ~ 10但是你却设置:
Near = 1 Far = 10那么这段真正重要的区域:
5 ~ 10只能获得:
NDC 0.778 ~ 1也就是整个[-1,+1]范围里很小的一段:
-1 ───────────────────────── 0.778 ──── 1 ↑ 5~10 全挤这里Near 拉得过近会让远处大量深度值挤在一起,而调整 Far plane 的影响通常小得多。developer.nvidia.com/blog/visualizing-depth-precision
因为 RenderDoc 本身就不是一个“GUI 程序里塞着所有功能”的架构。这个项目实际上做的是:直接把 RenderDoc 的 Replay Core 当 C++ library 使用,自己充当一个极简的 headless frontend(无界面前端)。
所以它不依赖renderdocgui/qrenderdoc.exe,但它仍然高度依赖 RenderDoc 本体的renderdoc.dll + Replay API。
可以把结构理解成:
官方 RenderDoc qrenderdoc.exe / GUI └─ CaptureContext / ReplayManager / Qt UI └─ RenderDoc Replay API └─ renderdoc.dll ├─ D3D11 replay ├─ D3D12 replay ├─ Vulkan replay ├─ OpenGL replay ├─ shader debug ├─ resource inspection └─ .rdc parser / replay machinery 这个 renderdoc-mcp renderdoc-mcp.exe └─ MCP / JSON-RPC └─ 自己写的 Session / Core wrapper └─ RenderDoc Replay API └─ renderdoc.dll也就是:
GUI 和 MCP 是两个并列的 frontend,而不是 MCP → GUI → RenderDoc。
它甚至在源码里非常直接地证明了这一点。
session.cpp:
C++ #include <renderdoc_replay.h>然后启动时直接:
C++ RENDERDOC_InitialiseReplay(env, args);打开.rdc:
C++ m_captureFile = RENDERDOC_OpenCaptureFile(); m_captureFile->OpenFile(...); auto [replayStatus, controller] = m_captureFile->OpenCapture(opts, nullptr);最后得到:
C++ IReplayController* m_controller;后面几乎所有 RenderDoc 能做的分析,都是围绕这个IReplayController展开的。github.com/JiaboLi-GitHub/renderdoc-mcp/blob/main/src/core/session.cpp
比如:
IReplayController ├─ GetRootActions() ├─ SetFrameEvent() ├─ GetPipelineState() ├─ GetD3D11PipelineState() ├─ GetD3D12PipelineState() ├─ GetTextures() ├─ GetBuffers() ├─ GetShader() ├─ PixelHistory() ├─ DebugPixel() ├─ DebugVertex() ├─ GetPostVSData() ├─ SaveTexture() └─ ...这些 API 本来就不是 GUI 专属 API。官方 RenderDoc 自己的 Python 示例甚至明确展示了完全不启动 GUI 的流程:
Python rd.InitialiseReplay(...) cap = rd.OpenCaptureFile() cap.OpenFile("test.rdc", ...) result, controller = cap.OpenCapture(...) controller.GetRootActions()所以 RenderDoc 官方从设计上就支持 standalone replay client。github.com/baldurk/renderdoc/blob/v1.x/docs/python_api/examples/renderdoc_intro.rst
这个项目真正比较值得注意的是,它没有走我们之前讨论过的那种:
AI ↓ MCP Server ↓ IPC RenderDoc GUI ↓ RenderDoc Python Extension ↓ pyrenderdoc而是直接:
AI ↓ stdio MCP renderdoc-mcp.exe ↓ C++ RenderDoc Replay API ↓ renderdoc.dll ↓ GPU replay