news 2026/10/22 3:33:10

D3D Hook通杀DirectX版本:挂点选择与工程实现指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
D3D Hook通杀DirectX版本:挂点选择与工程实现指南

简介:这是一份D3Dhook通杀方案的C++实现源码,面向DirectX图形开发者、逆向爱好者和游戏Mod制作者,解决覆盖Windows XP至8.1的跨版本Direct3D环境中插入自定义渲染逻辑、完成画面捕捉或渲染拦截的实际需求。整个rar压缩包仅2KB,内含1个cpp核心源文件,浓缩了API钩子与内存钩子两条技术路线的关键拦截逻辑。代码覆盖设备创建、交换链初始化、绘制调用等核心挂钩点,展示了DLL注入后替换Direct3D函数入口点的完整脉络,可帮助读者理解接口向后兼容性,并启发针对DirectX 12底层特性的进一步适配。虽然是精简示例,但对自研画面捕获工具、游戏增强插件或渲染性能分析模块的开发者而言,是一份能快速编译验证并二次改造的入门模板。该资源虽体积小巧,但信息密度较高,已有619人浏览学习。

1. “通杀所有DirectX版本”不是一句口号:D3D Hook的真实场景与适用范围

“D3Dhook通杀所有DirectX版本”这句话,第一次看到的人很容易理解成一个Hook库塞进任何D3D进程就能自动生效。实际上做过图形调试的都知道,这基本是玄学:D3D8/9是老的COM设备模型,D3D10/11把Present挪到了DXGI交换链上,D3D12干脆换成了CommandQueue,架构改了两代,根本不存在一个挂点能同时覆盖五个版本。所以通杀方案真正的含义,是“调度层通杀”:进程运行起来后先探测加载了哪个D3D runtime,再按版本路由到对应的Hook实现。做截帧工具、录屏、画面滤镜叠加层、帧延迟统计的开发者,需要的就是这套东西。

这套方案不复杂,但踩坑点非常多,尤其是“检测成功但回调不触发”“只在一个版本下有效”“一Hook就黑屏”这三类问题几乎每个人都遇到过。下文从版本差异讲起,给出一套能按步骤复现的工程骨架,把挂点选择、代码结构和坑位一次讲清楚。

2. 拆解“通杀”的含义:按D3D版本选Hook挂点,比传的玄学实在得多

2.1 D3D8/9/10/11/12的接口差异,决定了没有一个挂点能打天下

很多老教程把D3D Hook简写成“Hook Present”,这句话只对了一半。D3D8和D3D9时代,渲染结果通过设备对象上的Present方法提交,所以Hook的位置在IDirect3DDevice9::Present,这没错。到了D3D10和D3D11,Present被挪到了DXGI交换链上,也就是IDXGISwapChain::Present,你再去Hook设备对象的方法就什么都拦不到。D3D12更彻底,设备对象上根本没有Present,帧翻转发生在命令队列ID3D12CommandQueue::Present里,同时绘制命令的提交走的是ExecuteCommandLists。

所以版本之间的差异不是“换个函数名”这么简单,而是挂点对象整体变了。把五个版本放在一张表里看就很清楚:

版本主要runtime模块挂点对象需要Hook的核心方法备注
D3D8d3d8.dllIDirect3DDevice8Present / EndScene老游戏还能见到,32位为主
D3D9d3d9.dllIDirect3DDevice9Present兼容性最好的一代,虚拟桌面和帧调试常用
D3D10d3d10.dll + dxgi.dllIDXGISwapChainPresent挂点转移到了交换链
D3D11d3d11.dll + dxgi.dllIDXGISwapChainPresent同上,生产环境里最多
D3D12d3d12.dll + dxgi.dllID3D12CommandQueuePresent / ExecuteCommandLists需要区分命令队列和设备

看清这张表之后,所谓通杀就变成了一个工程问题:怎么在运行时识别版本,然后把同一个“帧事件”映射到不同挂点上。这个映射做得越干净,上层功能就越省事。

2.2 统一回调和版本适配层:把“不同版本”挡在功能层之外

我一般会在工程里加一个版本适配层,所有版本的Hook实现都只向调度层注册两个回调:帧提交前和帧提交后。上层做录屏、统计帧延迟、画叠加层的人,完全不感知当前进程是D3D9还是D3D12。这样做的直接好处是新增一个功能时不用写五份逻辑。

// hook_dispatcher.h 框架头文件示意 // 版本适配层对外暴露的帧回调结构 struct FrameHookCallback { DWORD runtime_version; // 8 / 9 / 10 / 11 / 12 void* device_context; // 设备或命令队列指针 UINT64 frame_index; // 帧序号,由调度层统一维护 HRESULT last_present_result; // 上一次Present的返回码 };

这个结构体是调度层和功能层之间的唯一契约。帧序号不要各版本自己维护,否则叠加层画出来的帧号对不齐。路由逻辑很简单:版本探测只做一次,回调注册也只在版本确定后执行一次,后续每次Present消息直接走帧回调。

2.3 为什么老帖子里写死的虚表偏移不能直接抄

D3D9时代很多帖子直接告诉你Present在设备虚表的第几个位置,抄过来也能用。但我强烈建议别把这个数字写死到工程里。原因有三个:第一,不同显卡驱动的D3D9设备虚表排列并不完全一致,N卡和A卡的老驱动在个别版本上有差异;第二,32位和64位进程的虚表布局不同,同一定位方法在两种进程里可能要调整;第三,驱动更新后虚表顺序可能变化,写死偏移会变成一个定时炸弹。

更稳的做法是“特征定位”:拿到设备虚表后,把每个函数指针取出来做粗筛,再根据函数的调用特征(参数数量、行为副作用)确认目标位置。这个方法在第4章展开。先记住结论:通杀方案不应该依赖任何固定偏移,它依赖的是“版本探测 + 挂点对象识别 + 行为特征匹配”。

3. 入口先行:从DLL注入到运行时版本探测的最小可跑通方案

3.1 注入环节的取舍:系统钩子与延迟加载的现实

D3D Hook必须运行在目标进程内部,所以第一步是把自己的DLL放进去。常见做法是写一个系统钩子DLL,通过SetWindowsHookEx挂到消息链上,目标进程加载钩子DLL后,我们就在DllMain里拿到执行机会。这个方式的优点是实现简单、系统集成度高;缺点是只能覆盖有消息循环的窗口进程,而且DllMain里不能做太多事情,否则容易在进程初始化阶段就把自己搞死。

另一个常见做法是写一个启动器,用CreateRemoteThread配合LoadLibrary注入。这种方式可控性更强,但需要处理32位和64位进程的差异,以及目标进程可能存在的安全拦截。我一般会在工具里同时保留这两种入口,用命令行参数控制。不管哪种方式,注入完成后都建议立即spawn一个独立线程去做后续工作,不要在DllMain里直接初始化D3D相关逻辑。

// dllmain.cpp 入口骨架,只做最小初始化 #include <windows.h> extern void dispatch_by_version(); // 定义在版本调度模块 BOOL APIENTRY DllMain(HMODULE mod, DWORD reason, PVOID reserved) { if (reason == DLL_PROCESS_ATTACH) { // 关键:不要在DllMain里做WaitForSingleObject或者LoadLibrary // 微软文档明确禁止,实际跑起来也容易死锁 HANDLE h = CreateThread(nullptr, 0, [](LPVOID) -> DWORD { dispatch_by_version(); // 探测版本并路由到对应Hook return 0; }, nullptr, 0, nullptr); if (h) CloseHandle(h); // 句柄不关也行,但关了更干净 } return TRUE; }

逻辑说明:DllMain只负责创建线程,把耗时工作全部丢到新线程里执行。dispatch_by_version是版本调度的核心入口,会在新线程里反复尝试探测D3D模块。这里有个新手经常犯的错:直接在DllMain里循环GetModuleHandle,结果进程还没初始化完,D3D模块列表一直是空的,等真正加载时线程已经退出了。

参数说明:CreateThread的第四个参数传0表示没有额外参数;返回的句柄在等待执行结束后可以CloseHandle,这里直接关闭不影响线程继续运行。DLL_PROCESS_ATTACH分支只做入口初始化,DLL_PROCESS_DETACH里一般要通知各版本Hook单元释放资源。

3.2 运行时探测D3D模块版本:三条判定路径

探测版本最直接的思路是检查目标进程加载了哪个D3D runtime模块。GetModuleHandle是一个很轻量的查询,它不会像LoadLibrary那样把模块强行加载进进程,所以是首选。

// d3d_version_detect.cpp #include <windows.h> // 返回当前进程加载的D3D主版本;返回0表示还没看到D3D runtime int detect_d3d_version_loaded() { // 按版本从新到旧查,避免低版本DLL伪装存在 if (GetModuleHandleW(L"d3d12.dll")) return 12; if (GetModuleHandleW(L"d3d11.dll")) return 11; if (GetModuleHandleW(L"d3d10.dll")) return 10; if (GetModuleHandleW(L"d3d9.dll")) return 9; if (GetModuleHandleW(L"d3d8.dll")) return 8; return 0; }

逻辑说明:这里按版本从高到低查是刻意的。有些程序同时链接了d3d9.dll和d3d11.dll,比如兼容性启动器,此时应该以更高版本为准,因为实际渲染走的是高版本路径。如果你遇到一个程序确实同时加载了两个版本,需要进一步判断具体哪个设备在渲染,这种场景放在第5章的排查清单里说。

参数说明:GetModuleHandleW的参数是模块文件名,不带路径,也不带.dll后缀之外的任何扩展。它只返回模块的基址,如果模块没加载返回NULL。注意这里并不会触发模块加载,所以是只读的探测,是安全的。

3.3 版本分发调度:等不到模块时的轮询策略

版本探测不是一次就能成功的。D3D模块往往是应用启动后才加载,尤其是D3D12,很多引擎在创建设备前根本不会加载d3d12.dll。如果只探测一次就放弃,注入后看起来就是“完全没反应”,这是D3D Hook里最常见的假失败。

// dispatcher.cpp 版本分发,带超时轮询 #include <windows.h> int detect_d3d_version_loaded(); extern void hook_d3d9(); extern void hook_d3d10(); extern void hook_d3d11(); extern void hook_d3d12(); // 每隔100ms探测一次,最多等5秒 void dispatch_by_version() { for (int attempt = 0; attempt < 50; ++attempt) { int v = detect_d3d_version_loaded(); switch (v) { case 12: hook_d3d12(); return; case 11: hook_d3d11(); return; case 10: hook_d3d10(); return; case 9: hook_d3d9(); return; default: break; } Sleep(100); } // 5秒还没等到,说明目标进程可能不用D3D,或者加载路径特殊 // 此时可以再挂一个LOAD_LIBRARY通知,但那是进阶做法,见第6章 }

逻辑说明:轮询的目的是覆盖延迟加载场景。Sleep(100)把CPU占用压到几乎没有,50次循环覆盖5秒窗口,对绝大多数应用足够。如果超过5秒还没等到D3D模块,基本可以判定目标进程没有走标准D3D加载路径,继续等下去意义不大。

参数说明:轮询间隔和总次数是可调参数。间隔太短CPU空转,太长会错过一些启动后很久才初始化的程序。我常用的组合是100ms间隔、最大5秒窗口,特殊情况用命令行参数放开到10秒。这里还有一个细节:hook_d3d9()这些函数内部会各自处理“是否已Hook过”的幂等判断,避免dispatch被重复调用时挂两次。

4. 三种挂点实现:设备虚表、交换链、命令队列的Hook与调用链复原

4.1 D3D8/9:设备虚表上定位Present,不依赖固定偏移

D3D8和D3D9的Hook思路一致:拿到IDirect3DDevice9对象后,读取它的虚表,找到Present方法的函数指针,替换成自己的实现。问题在于怎么找到Present在虚表里的位置。前面说过不建议写死偏移,这里给出具体的定位思路。

// d3d9_hook.cpp 虚表特征定位骨架 #include <windows.h> #include <d3d9.h> // 把虚表函数指针的代码段归属和调用行为结合起来判断 static int LocatePresentIndex(IDirect3DDevice9* dev) { void** vtbl = *reinterpret_cast<void***>(dev); // 不要写死固定索引,遍历虚表前半部分,行为特征最可靠 for (int i = 0; i < 64; ++i) { BYTE* fn = reinterpret_cast<BYTE*>(vtbl[i]); // 粗筛:函数指针必须落在已加载模块的代码区,而不是数据区 MEMORY_BASIC_INFORMATION info = {}; if (VirtualQuery(fn, &info, sizeof(info)) == 0) continue; if (info.State != MEM_COMMIT) continue; if (info.Protect == PAGE_NOACCESS) continue; // 粗筛通过后,用一个探针调用做行为确认: // 传一个空窗口句柄,预期返回D3DERR_INVALIDCALL而不是崩溃 typedef HRESULT(WINAPI* PresentFn)(IDirect3DDevice9*, CONST RECT*, CONST RECT*, HWND, CONST RGNDATA*); PresentFn probe = reinterpret_cast<PresentFn>(fn); HRESULT hr = probe(dev, nullptr, nullptr, nullptr, nullptr); if (hr == D3DERR_INVALIDCALL) { return i; // 这个索引的设备方法接受上述参数且按预期返回错误码 } } return -1; }

逻辑说明:这里的核心不是具体索引,而是“行为特征”三个字。调用探针传空参数,如果函数真的是Present,它会因为参数不合法返回D3DERR_INVALIDCALL;如果函数是别的设备方法,表现完全不一样。这个做法的优点是显卡驱动怎么更新都不影响正确性。缺点是不能频繁调用,尤其是对已经处于渲染中的设备,探针调用本身有副作用,所以只在初始化阶段定位一次。

参数说明:虚表遍历范围取64项是经验值,D3D9设备接口方法总数远小于这个数,留了余量。VirtualQuery用来确认函数指针落在合法代码区,防止虚表里混入空指针或特殊标记。探针调用里四个参数全传nullptr,就是为了让Present走到参数校验分支,从而用一个安全的错误码确认身份。

4.2 D3D10/11:挂DXGI交换链,不在设备上浪费时间

D3D10和D3D11的Present不挂在设备对象上,挂在IDXGISwapChain上。所以Hook的时机不是“创建设备后”,而是“创建交换链后”。常见做法是同时Hook DXGI的CreateSwapChain/CreateSwapChainForHwnd,拿到返回的交换链指针后,再Hook它的Present。

// dxgi_hook.cpp 交换链Present的Hook主体示意 #include <windows.h> #include <dxgi.h> // 原始函数指针,由inline hook框架保存 typedef HRESULT(WINAPI* PFN_DXGI_PRESENT)(IDXGISwapChain*, UINT, UINT); PFN_DXGI_PRESENT g_orig_present = nullptr; // 替换后的Present:先上报帧开始,再调用原函数 HRESULT WINAPI HookPresent(IDXGISwapChain* swapchain, UINT sync_interval, UINT flags) { // 上报帧事件,功能层在这里做帧统计和叠加层准备 OnFrameBefore(swapchain, sync_interval, flags); // 调用原有Present,参数必须原样传递,不能吞掉 HRESULT hr = g_orig_present(swapchain, sync_interval, flags); // 帧提交完成,功能层在这里做后处理 OnFrameAfter(swapchain, hr); return hr; }

逻辑说明:Hook函数里最忌讳的是对参数做“优化”。sync_interval控制垂直同步,flags里有DXGI_PRESENT_TEST等特殊标记,如果一个都没改,整体行为才和原来一致。g_orig_present是inline hook框架在替换时保存下来的原始函数入口,必须通过它回到原始调用链,否则会递归死循环。

参数说明:sync_interval为0表示立即显示不等待垂直同步,为1或更大表示要等若干个垂直同步周期;flags里比较常用的是DXGI_PRESENT_TEST(只测试不真正翻转)和DXGI_PRESENT_DO_NOT_WAIT(不阻塞等待)。如果功能层要做图形叠加,这些参数的语义必须理解清楚。

4.3 D3D12:设备上没有Present,CommandQueue才是挂点

D3D12是改动最大的一代。设备和交换链接口里都没有Present方法,帧翻转发生在ID3D12CommandQueue::Present上。这意味着Hook流程变成:等待d3d12.dll加载,拿到设备的命令队列,再在命令队列对象上Hook Present。同时,如果要捕捉画面内容,还得关注ExecuteCommandLists,因为Present本身不携带渲染结果,渲染结果在命令列表里。

// d3d12_hook.cpp 命令队列Present的Hook骨架 #include <windows.h> #include <d3d12.h> typedef HRESULT(WINAPI* PFN_CMDQUEUE_PRESENT)(ID3D12CommandQueue*, UINT, UINT); PFN_CMDQUEUE_PRESENT g_orig_queue_present = nullptr; HRESULT WINAPI HookCmdQueuePresent(ID3D12CommandQueue* queue, UINT sync_interval, UINT flags) { // D3D12在Present之前,通常还有若干ExecuteCommandLists在等待 // 需要先用ID3D12Fence等待渲染真正完成,才能拿到可用的后缓冲 OnD3D12FrameBegin(queue); HRESULT hr = g_orig_queue_present(queue, sync_interval, flags); // 注意:此刻GPU工作不一定结束,功能层若需要读像素, // 必须用Fence同步,不能在CPU线程直接读 OnD3D12FrameEnd(queue, hr); return hr; }

逻辑说明:D3D12的异步特性是最大的坑。CPU侧调用Present后,GPU可能还在执行前一批命令,如果此时去读后缓冲内容,拿到的可能是上一帧甚至半帧数据。正确的做法是在OnFrameEnd里用Fence插入一个信号量,然后等待信号量触发,再开始读像素。这个Fence的创建和使用需要额外的几十行代码,这里不展开,但理解这个时序比看懂Hook函数本身更重要。

参数说明:ID3D12CommandQueue::Present的两个参数和DXGI Present含义一致,sync_interval控制等待垂直同步的节拍数,flags保留字段。OnD3D12FrameBegin里可以做帧计数和耗时统计,但不建议直接做任何GPU资源操作,因为命令队列还在关键路径上。

4.4 三个挂点的参数差异对照

把三个挂点放在一起对比,能看出调用约定和参数的大致区别,也方便排查“为什么这个版本下回调参数不对”:

挂点方法关键参数常见用途需要额外注意的
IDirect3DDevice9::PresentpSourceRect / pDestRect / hDestWindowOverride / pDirtyRegion只关心帧翻转时机hDestWindowOverride可能为空,表示默认窗口
IDXGISwapChain::PresentSyncInterval / Flags帧统计、叠加层Flags带TEST时不要做叠加绘制
ID3D12CommandQueue::PresentSyncInterval / Flags帧统计、Fence同步GPU异步,读像素必须等Fence

三个挂点有一个共同原则:原函数的参数和返回值必须原样透传。任何一层吞掉参数或者修改返回码,都会导致目标程序出现黑屏、撕裂或者卡死,这类问题排查起来极其消耗时间,最好的策略就是从第一版代码就遵守这条纪律。

5. D3D Hook通杀方案避坑指南:从黑屏到注入后无效果的排查清单

5.1 注入后画面黑屏,或渲染线程直接卡死

现象:DLL注入成功,Hook也触发了,但目标程序画面黑屏、窗口无响应,或者渲染线程卡在一个函数里不动。

原因:最常见的是在Present回调里做了阻塞操作。Present本身在渲染线程的关键路径上,如果你在回调里Sleep、WaitForSingleObject、加一个被其他线程长期持有的锁,渲染线程就会停摆。另一个常见原因是探针调用太频繁,或者第一次定位Present时误判了函数,在错误的位置做了inline hook,直接把代码写坏了。

解决:先保证“帧回调里不做任何阻塞操作”。锁用短临界区,数据处理交给单独的工作线程,帧回调只做标记和入队。同时把虚表定位过程加上日志,打印定位到的索引和探针返回码,确认定位正确后再启用真正的Hook。

5.2 注入后毫无反应,回调从来没触发过

现象:DLL被加载了,DllMain执行了,dispatch函数也跑了,但回调一次都没触发。

原因:大概率是版本探测时机太早,D3D模块还没加载。前面说过,很多引擎是启动后才加载d3d11.dll或d3d12.dll,如果你在注入后立刻探测一次就返回了,就永远等不到模块加载。还有一种情况是目标程序用的是D3D10.1,但你的探测逻辑只查了d3d10.dll,实际进程加载的是d3d10_1.dll。

解决:把dispatch_by_version改成轮询版本,见3.3节的代码。对D3D10.1这种变体,在探测逻辑里加上d3d10_1.dll的检查,并且把它归到版本10的Hook路径上。另外加一个日志点,每次探测失败都记录一条当前已加载模块列表,能帮你在五分钟内定位问题。

5.3 Hook后出现递归死循环,栈溢出崩溃

现象:代码能编译能跑,一旦触发Present就递归调用直到栈溢出,或者程序在release模式下随机崩溃。

原因:这是最经典的inline hook翻车现场。替换函数里调用“原函数”时,如果拿到的地址是已经被修改过的函数入口,就会再次进入Hook函数,形成无限递归。常见诱因是:没有保存原始函数入口,而是重新解析了一遍模块导出表;或者保存原始入口的变量被多次赋值覆盖。

解决:用现成的inline hook框架来管理原始函数入口,不要自己手动修改函数头字节。这类库会正确保存被覆盖的指令并生成一个中转函数,你只需要调用保存的原始指针即可。代码审查时重点确认:g_orig_present只在安装Hook时赋值一次,之后不再改动。同时避免在多个版本之间共享同一个原始函数指针变量。

5.4 Debug运行时下崩溃,Release却正常

现象:同一份代码,在Debug配置下编译后注入就崩溃,Release配置下运行稳定。

原因:D3D的Debug Layer会启用更严格的参数校验和不同的内部对象布局,部分驱动在Debug运行时下虚表排列也会变化,甚至调试器附加状态都会影响时序。如果你恰好做了探针调用,Debug运行时可能对非法参数返回的不是预期错误码,导致定位失败,然后Hook写到了错误的位置。

解决:不要在Debug配置下验证D3D Hook的稳定性。开发阶段可以用Debug配置做日志和断点调试,但“通杀”结论必须基于Release配置、非附加调试器的运行状态来下。如果你确实需要两套配置都稳定,就要求虚表定位逻辑不依赖具体返回码,而是结合多个行为特征做综合判断。

5.5 窗口化正常,全屏独占模式下回调不触发

现象:窗口模式下一切正常,切到全屏独占模式后Hook立刻失效,或者只触发一次就再没有回调。

原因:DXGI全屏切换时,交换链会被销毁并重建,旧交换链上的Hook点自然就失效了。全屏独占模式下Present的调用路径和窗口模式也有差异,某些驱动会走不同的内部函数,导致inline hook安装的位置没有命中。

解决:监听交换链的Resize事件和窗口消息,在检测到全屏切换时重新获取交换链指针并重新安装Hook。另外在全屏模式下不要用窗口句柄做任何关联判断,Present的hDestWindowOverride参数在全屏路径下常常是空的,处理逻辑要用默认窗口的行为而不是强制读句柄。

6. 进阶:用自动化验证把“通杀”变成可信指标——路由打点与帧号校验

等五个版本都能触发回调之后,下一步就是把“通杀”从口头说法变成可验证的数据。我习惯在三类目标程序上做自动化验证:一个旧版D3D9示例程序、一个D3D11窗口程序、一个D3D12示例,分别注入后跑固定时长的渲染循环,然后对比三项指标:

验证指标正常值异常说明
版本路由日志只记录一个主版本同一进程出现多个版本,路由可能出错
帧计数递增每秒帧数与程序帧率一致计数不涨说明挂点不对或者回调阻塞
原始Present返回码与未Hook时的程序行为一致返回码被改过,说明透传有问题

跑验证的方式很简单:在Hook初始化完成后往日志写一行版本信息,在每次回调里给帧计数加一,并记录Present的返回值。跑完三分钟,日志里应该出现三条清晰曲线:版本号、帧号递增、返回码稳定。如果某个版本下帧号跳变或者长时间不涨,优先怀疑那个版本的挂点定位逻辑。

验证细节上还有一个小习惯:先做“只统计不绘制”的灰度跑法,确认帧率没有掉损失后再叠加滤镜绘制。很多人一开始就同时做Hook和渲染叠加,出了问题分不清是Hook掉了帧还是渲染叠加层开销太大。只统计不绘制的模式可以把两条线彻底分开。

如果你连“等D3D模块加载”都觉得不够优雅,还可以往工程里加一个LoadLibrary通知回调,在模块真正加载的瞬间触发路由,替代固定轮询。这个方案更及时、CPU占用更低,但代码复杂度高一截,我一般先用轮询跑通整体链路,再加入通知机制优化启动延迟。D3D Hook这套东西,最大的教训就是“先求稳定再求优雅”,任何一步跳过验证就上全量叠加,后面排查的时间会翻倍。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:33:07

深度学习张量转PSD分层可视化:从BCHW到可编辑文档的工程实践

我得先承认一个听起来不太搭的组合&#xff1a;把深度学习里的张量&#xff0c;直接画成 Photoshop 的 PSD 文件。这块工具最后被命名为 draw_tensor2psd_v4.py&#xff0c;最新跑出来的结果文件夹叫“结果0129”&#xff0c;里面是模拟项目 X 的一次语义分割训练在验证集上的完…

作者头像 李华
网站建设 2026/10/22 3:33:04

2026年构建AI Agent完整路线图:从Prompt到工程化

2026年再谈"从零构建 AI Agent"&#xff0c;听起来有点自相矛盾——这个词已经被翻来覆去讲了两年多&#xff0c;教程铺天盖地&#xff0c;感觉再不进场就落后了。但我这几年看过不少团队和个人开发者做 Agent 项目&#xff0c;遇到的最大问题根本不是"找不到资…

作者头像 李华
网站建设 2026/10/22 3:33:03

UE C++反射宏全解析:从UCLASS到UPROPERTY的进阶指南

做虚幻引擎C开发&#xff0c;绕不开一堆以U开头的宏。新手第一次看到 UCLASS()、UPROPERTY(VisibleAnywhere)、GENERATED_BODY() 挤在一起时&#xff0c;多半会愣一下&#xff1a;这些到底是语法的一部分&#xff0c;还是某种编译器魔法&#xff1f;“说明宏”这个名字听起来像…

作者头像 李华
网站建设 2026/10/22 3:32:00

STM32+ESP8266智能家居系统:从底层驱动到MQTT联网实战

简介&#xff1a;基于STM32与ESP8266的智能家居系统完整工程代码&#xff0c;面向物联网爱好者、嵌入式开发者及企业智能化项目人员。系统以STM32为核心采集温湿度、光照、烟雾等传感器数据&#xff0c;借助ESP8266 Wi-Fi模块上云&#xff0c;并通过微信小程序实现远程监控与设…

作者头像 李华
网站建设 2026/10/22 3:31:36

大文件分片上传原理与工程实践:可靠性优先的上传架构

1. 什么是大文件分片上传&#xff1a;不是“传得快”&#xff0c;而是“传得稳、传得全、传得可控”你有没有遇到过这样的场景&#xff1a;在某个系统里点下“上传”按钮&#xff0c;选中一个2GB的工程视频、一个800MB的医学影像数据包&#xff0c;或者一个1.5GB的CAD三维装配体…

作者头像 李华
网站建设 2026/10/22 3:31:35

BD-RIS超越对角线:MIMO容量最大化与Matlab仿真实践

做通信仿真这几年&#xff0c;我越来越发现一个现象&#xff1a;很多人一提到RIS&#xff08;可重构智能表面&#xff09;&#xff0c;脑子里还是“一块可以调相位的镜子”&#xff0c;觉得它就是把每个反射单元的相位旋转一下&#xff0c;让信号更集中。这个理解没有错&#x…

作者头像 李华