前阵子有个做工具链的朋友把一个卡了三天的工程丢给我,开口第一句是"我照着某套老教程抄的,怎么一开 Debug Layer 就刷红字"。我打开一看,问题不在他写的逻辑,而在于他学的那套 DX12 教程是七八年前的版本:D3D12CreateDevice后面紧跟一堆今天已经被运行时收严的写法,屏障用GENERIC_READ通吃,描述符堆建成了非 shader-visible,纹理上传还按 D3D11 的思路Map完直接当 SRV 用。这篇文章就是我按"从 Device 到贴图三角形"这条 25 集的路线,把图形学里最劝退的图形 API 入门环节重新捋一遍,包含真实调试过程。适合两类人:一是有 C++ 和线性代数基础、想认真学 DX12 但被老教程带偏的人;二是已经能画三角形、但一碰纹理和调试就抓瞎的人。
1. 老教程到底哪里过期了:DX12 入门最容易被教歪的四个点
1.1 运行时版本和 SDK 之间有一段你看不见的错位
这是最隐蔽的一层。DX12 的运行时是随 Windows 一起分发的,你装完系统就已经有了d3d12.dll,但它只保证一份"基底能力"。而新特性——比如增强屏障(Enhanced Barriers)、Shader Model 6.6 之后的一批指令、Work Graphs——需要靠 Agility SDK 把新的D3D12Core.dll随程序一起分发出去,程序启动时再通过D3D12SDKVersion和D3D12SDKPath告诉运行时"我要用旁边这个版本"。
老教程写于 2016 到 2018 年,那时候这些机制还不存在,教程里的D3D12CreateDevice返回一个ID3D12Device就万事大吉。今天你如果只拿到ID3D12Device,后面想用ID3D12GraphicsCommandList7做增强屏障、想查D3D12_FEATURE_D3D12_OPTIONS12,就得层层QueryInterface上去,而教程里完全没有这一步。结果就是:代码能编译,运行时拿到E_NOINTERFACE,然后新手开始怀疑自己是不是漏抄了哪一行。
我的建议很直接:入门阶段先不引入 Agility SDK,用系统自带运行时把基础链路走通,把屏障、描述符、PSO 这三件事弄明白;等你真正需要新特性时,再接 SDK,并且记住一个硬规则——SDK 版本号要和你放出去的D3D12Core.dll严格对应,版本号写错会直接启动失败。
1.2 那些"能跑但不该学"的写法
有几个写法在老教程里出现频率极高,跑起来好像也没问题,但你一旦深入就会被反噬,我把它们列出来,你对着自己的代码自查:
- 用
D3D12_RESOURCE_STATE_GENERIC_READ或COMMON当万能状态。老教程为了避免写屏障,干脆让资源一直待在万能状态。Debug Layer 会给你警告,性能上驱动也可能给你降到最保守的路径。正确的做法是老老实实写转换:上传堆是GENERIC_READ,默认堆的纹理从COPY_DEST转到PIXEL_SHADER_RESOURCE。 - 把所有资源在初始化时一次性创建完。这是 D3D11 时代的肌肉记忆。DX12 里资源创建走的是 placed resource 加子分配这套思路才是主流,但入门阶段用
CreateCommittedResource没问题,重点是你得知道还有另一条路。 - 用 fxc 编译 shader。fxc 只支持到 SM 5.1,DX12 该用 DXC(
dxcompiler.dll),编译出 DXIL。用 fxc 你会在写 SM 6.0 特性时撞墙。 - 不做设备移除(DRED)配置。这是新手和老手最大的分水岭,我在第 5 节会把完整链路讲透。
- 不检查 Feature Level。
D3D12CreateDevice的第二个参数如果写D3D_FEATURE_LEVEL_11_0,你会在部分集显上得到硬件不支持的结果却不知道原因。
1.3 为什么这条路线要从 Device 切入
很多人问:为什么不先讲渲染管线、不讲三角形光栅化,直接从画线画三角形开始?因为 DX12 的报错机制决定了你必须先把 Device 建对。后面所有东西——每个屏障、每个描述符、每个 PSO——都挂在 device 上,Device 层出问题,后面每一步的报错都会指向错误的地方,形成连锁误报。你会在一个"纹理采样拿不到数据"的报错里,花了三天去改 HLSL,最后发现是 device 创建设置让 GPU-Based Validation 根本没打开。
从 Device 起步还有一个好处:它能让你养成"每加一个能力就问运行时一次"的习惯。CheckFeatureSupport这个函数在后面的 25 集里会反复出现,从贴图格式支持、到采样器各向异性、到增强屏障是否可用,答案都在运行时里,不在教程里。
2. 初始化链路:从 D3D12CreateDevice 到能 Present 的第一帧
2.1 适配器枚举别写死索引 0
老教程最常见的写法是factory->EnumAdapters(0, &adapter)。这在单显卡台式机上没事,在双显卡笔记本上就是灾难:索引 0 往往是核显,你的独显压根没被选中,然后你还奇怪为什么帧率只有十几。正确做法是用IDXGIFactory6::EnumAdapterByGpuPreference,传DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,让系统按性能偏好给你挑。如果拿到的适配器不是你要的那块(比如用户接了外置显卡坞),再枚举一遍,比对显存大小或者 LUAD 标识。
同时要处理一个现实问题:D3D12CreateDevice返回E_INVALIDARG说明这块适配器不支持你请求的 Feature Level。这时候不要直接崩,先用D3D12_FEATURE_LEVEL_11_0降级试一次,还不行就用 WARP(软件光栅化适配器)跑起来。WARP 速度很慢,但它是排查"到底是硬件不支持还是我代码有 bug"的利器——我调试贴图采样问题时,就靠 WARP 确认过是驱动侧的格式处理差异。
2.2 Debug Layer 和 GPU-Based Validation 的开启顺序
这里有个顺序问题,踩过的人才知道:所有调试相关的开关都必须在D3D12CreateDevice之前设置,之后你再开就是白开。
// 必须在创建设备之前 ID3D12Debug* debug; D3D12GetDebugInterface(IID_PPV_ARGS(&debug)); debug->EnableDebugLayer(); ID3D12Debug3* debug3; if (SUCCEEDED(debug->QueryInterface(IID_PPV_ARGS(&debug3)))) { debug3->SetEnableGPUBasedValidation(TRUE); debug3->SetEnableSynchronizedCommandQueueValidation(TRUE); }顺带说一个 Windows 上的坑:D3D12GetDebugInterface需要系统装了"图形工具"这个可选功能,精简版系统上会返回失败。如果你发现这个调用拿不到对象,先去系统设置里把可选功能补齐,别在那里怀疑代码。
GPU-Based Validation 比 Debug Layer 更狠也更慢,它会把 GPU 侧实际执行的访问和资源状态对一遍。我的习惯是:日常开发只开 Debug Layer,一旦出现状态类诡异问题,再临时打开 GPU-Based Validation,因为它能让一类"CPU 侧看不出来、GPU 侧才炸"的问题直接现形。
2.3 命令队列、Fence 与"帧在飞"数量的确定
DX12 的帧循环骨架其实就三件事:录制、提交、等信号。ID3D12CommandQueue::ExecuteCommandLists把命令丢给 GPU,ID3D12Fence::Signal打一个标记值,CPU 端在下一帧开始时Wait上一个帧的标记值,确保不会覆盖 GPU 还在读的数据。
"帧在飞"(frames in flight)的数量就是你能容忍 CPU 跑到第几帧时 GPU 还没画完。常见取值是 2 到 3,也就是双缓冲或三缓冲。为什么不是越多越好?因为每多一帧在飞,你就要多一套命令分配器(allocator)和命令列表(command list),还要多一份常量缓冲或上传缓冲,内存会线性增长;而且输入的响应延迟会跟着变大。三帧在飞在大多数场景里是一个不错的平衡点。
有个细节必须记住:fence 值不要只用一个变量。你要给每个帧槽位记一个"这一帧提交时的 fence 值",等的时候等的是对应槽位的值,而不是最后那个全局值。很多人这里写错,表现是"偶尔画面撕裂或者资源被提前覆盖",而且很难复现。
2.4 SwapChain 配置里的三个易错参数
第一,交换效应必须是DXGI_SWAP_EFFECT_FLIP_DISCARD,这是唯一被推荐的现代模式,老教程里的DISCARD(非 FLIP)会让 DXGI 走兼容路径,性能差且会有一堆额外限制。第二,BufferCount建议 3,配合DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING可以支持可变刷新率,但要注意打开这个标志后Present必须传DXGI_PRESENT_ALLOW_TEARING,并且这个标志只有在全屏或特定条件下才真正生效,窗口模式下要自己判断。第三,ResizeBuffers之前必须释放所有对后备缓冲(back buffer)的引用——这个坑我踩过,报错是DXGI_ERROR_INVALID_CALL,看起来跟尺寸毫无关系,实际原因就是某个ComPtr<ID3D12Resource>还持有旧的后备缓冲。
另外记得调IDXGISwapChain3::SetMaximumFrameLatency,默认值在某些驱动上偏大,会引入额外的输入延迟,1 到 2 是比较顺手的选择。
3. 命令录制与资源状态:DX12 最反直觉的一层
3.1 CommandAllocator 的复用红线
命令分配器的规则只有一条,但违反了就必炸:不要 Reset 一个 GPU 可能还在读的分配器。所以每个帧槽位配一个分配器,只有在确认对应帧的 fence 已经完成之后,才能Reset它然后重新录制。
还有一个容易忽略的点:分配器和命令列表不是一对一的。一个分配器在同一时刻只能被一条命令列表使用,但你可以让一条命令列表每帧Reset到不同的分配器上。这个设计的目的就是让你能"一边录制下一帧,一边让 GPU 画当前帧"。我第一次理解这个机制时,脑子里想的是"这不就是给录制工作准备了几个草稿本,写满一个就换下一个,等 GPU 看完旧草稿再擦掉重写"——这个类比后来帮我给好几个人解释清楚了。
3.2 Resource Barrier 的合并与拆分
屏障(barrier)是 DX12 性能调优里最值得下功夫的地方,因为它直接决定 GPU 是否要停下来等。两条实践原则:
- 把相邻的转换合并到一次调用里。
ID3D12GraphicsCommandList::ResourceBarrier接受一个数组,你把同一时刻要转的资源一起传进去,驱动能做更好的调度规划,比连续调用十次要强。 - 状态的粒度要按实际用途来定,不要为了省事让一张纹理一直待在
PIXEL_SHADER_RESOURCE。如果它还要被当作渲染目标写过,那就必须显式转换过去再转回来。
关于新出的增强屏障(Enhanced Barriers),它用同步范围、访问类型、布局三件套替换了原来单一的状态枚举,表达能力强很多,比如可以精确描述"只读的深度附件"这种中间态。但我的建议很明确:先把老式屏障用明白。因为增强屏障需要特性检查通过才能用,而且一旦混用,排错难度会翻倍。
3.3 Descriptor Heap 的分配策略
描述符堆有两条完全不同的规则,混在一起讲最容易乱。RTV、DSV 这类堆不是 shader-visible 的,你只能在 CPU 侧写;CBV/SRV/UAV 要做成 shader-visible 的,GPU 才能读。老教程里把 SRV 写进非 shader-visible 的堆里,运行时给你一句"指定的描述符堆不可被着色器访问",新手看着这句话完全不知道从哪改。
分配策略上,我推荐两段式:一部分是"每帧线性分配"的堆,用来放常量缓冲视图、每帧变化的 SRV,帧开始时把指针归零重新分配,因为 GPU 在用它,所以要按帧槽位轮转;另一部分是"持久引用"的堆,用来放长期不变的贴图 SRV、采样器之类,创建一次就不再动。描述符的大小不要靠猜,用GetDescriptorHandleIncrementSize拿。
这里顺便说个性能上的细节:描述符堆切换是有成本的,所以按用途分堆、每帧尽量少切换,比把上百个描述符塞进一个大堆然后频繁切换更划算。
3.4 Root Signature 1.0 与 1.1 的实际差别
根签名是 DX12 里最像"编译期契约"的东西。它定义了着色器怎么看到资源:根常量、根描述符、描述符表、静态采样器。1.1 版本加了几个实用能力,其中静态采样器最值得用——把采样器直接编进根签名,不用再为它单独建堆。还有描述符范围上的DATA_STATIC和DATA_VOLATILE标志,标成 STATIC 的可以帮驱动做更强的优化。
有一个必踩的坑:如果你要画带输入布局的几何体,根签名必须带D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,否则DrawIndexedInstanced会直接报错,而且报错信息不一定直白地提到根签名。
4. 贴图三角形落地:从 WIC 解码到 PS 里采出第一张图
4.1 纹理上传的完整链路与 256 字节行对齐
上传一张 PNG 到 GPU,标准链路是六步,每一步都有坑:
- 用 WIC 解码成 32 位 RGBA 像素数据。这里要注意 WIC 给出的
stride和你后面要做的行拷贝是两码事。 - 用
ID3D12Device::GetCopyableFootprints算出每个子资源的行间距(RowPitch)、总字节数和放置位置。行间距是按 256 字节对齐的,所以一张 1024 宽的 RGBA 贴图,一行 4096 字节刚好对齐,但一张 1000 宽的贴图,一行 4000 字节就会被拉到 4096,多出的 96 字节是填充。 - 建一个上传堆(
D3D12_HEAP_TYPE_UPLOAD)的缓冲,状态是GENERIC_READ。 - 建一个默认堆的纹理资源,初始状态
COPY_DEST。 Map上传缓冲,按行逐行拷贝,源地址按 WIC 的 stride 走,目标地址按 footprint 的 RowPitch 走。这一步如果偷懒用一次memcpy拷整块,画面就会出现斜的错位,这是最经典的"看起来像显卡坏了"的症状。CopyTextureRegion提交拷贝,然后插屏障把纹理转到PIXEL_SHADER_RESOURCE,最后CreateShaderResourceView。
我强烈建议第 5 步不要"省一次循环",因为你省下的那点 CPU 时间,换来的是几小时的排查。
4.2 sRGB、格式与"Blender 里好好的贴图"为什么在引擎里翻车
有个高频问题:贴图在 Blender 或其他 DCC 工具里看着完全正常,导出到引擎里就发灰、发暗、或者颜色明显偏。九成原因是 sRGB 编解码的归属搞错了。
颜色贴图在美术工具里通常是以 sRGB 编码存储的,GPU 采样时需要硬件帮你转成线性空间参与光照计算,做法就是把 SRV 的格式设成DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;而法线贴图、粗糙度、金属度这类数据贴图必须是UNORM,因为它们存的是数值不是颜色,再给你来一次 sRGB 转换就全错了。这两类贴图经常被随手统一处理,然后就出现"光照怎么调都不对"的情况。
反过来,渲染目标(RTV)用不用 sRGB 格式,要和你的后处理链路统一。如果你想在渲染目标里存线性值、最后统一做色调映射,那 RTV 用UNORM;如果直接输出到交换链,通常最终呈现要走 sRGB 编码。方案不能混着来,混着来就会出现"画面整体偏亮但某个物体正常"这种诡异现象。
4.3 Sampler、mipmap 与采样质量
采样器看着参数一大堆,入门阶段你只要把这几项调对:
| 参数 | 建议值 | 说明 |
|---|---|---|
| Filter | D3D12_FILTER_ANISOTROPIC | 地面、斜视角贴图质量差异极大 |
| MaxAnisotropy | 8 或 16 | 越高越清晰,代价是采样带宽 |
| AddressU/V/W | WRAP或CLAMP | 平铺用 WRAP,单张用 CLAMP |
| MaxLOD | D3D12_FLOAT32_MAX | 写 0 会导致没有 mip 可用时报错 |
| MipLODBias | 0 | 调锐度时才动 |
| ComparisonFunc | D3D12_COMPARISON_FUNC_NEVER | 普通采样器不用比较功能 |
| BorderColor | 按需 | 只有 CLAMP 加边界模式才用得上 |
mipmap 这件事,入门阶段我建议直接让美术导出时带 mip,别在运行时用 compute 现生成——不是因为不能做,而是因为你还同时要调 PSO、调屏障、调上传,一次改太多变量,出问题你根本分不清是哪一步的锅。等你基础链路稳了,再单独做一期 GPU 生成 mip 的练习。
4.4 PSO 里的必填项与 UV 插值方向
PSO 创建失败最常见的原因是结构体里某个字段留了默认值。输入布局里的语义名(POSITION、TEXCOORD)必须和 HLSL 里的语义名严格一致,不一致的后果分两种:如果 PSO 创建时报错,那还是幸运的;如果只是某个通道读到了垃圾数据,你会看到画面扭曲或者全黑,排查起来非常痛苦。
UV 方向也是新手必踩的一脚:D3D 的纹理坐标原点在左上角,v 轴向下。你自己手写的顶点数据如果按数学课本的直觉把 v 向上给,贴图就是上下颠倒的。两种修法——在顶点数据里就按 D3D 约定给,或者在像素着色器里对 v 取1 - v。我个人推荐前者,因为把"数据约定"和"渲染逻辑"分开,后面接模型文件时更顺。
还有两个字段:SampleDesc.Count如果你没打算用 MSAA 就必须是 1,NumRenderTargets要和 RTV 的实际数量对齐,DSVFormat留空的话必须显式写成DXGI_FORMAT_UNKNOWN,别留零值碰运气。
5. 调试全记录:把报错当线索而不是噪音
5.1 Debug Layer 的报错怎么读:第一个屏障冲突
我印象最深的一次,Debug Layer 刷了三十多条错误,新手容易从最后一条开始看,结果越看越乱。正确做法是只读第一条,因为后面的报错往往是第一条的连锁反应。
那次第一条的意思是:某个资源在绘制命令里被当作顶点缓冲读取,但它当前的状态不是VERTEX_AND_CONSTANT_BUFFER,而是别的东西,所以这次读会被忽略。翻译成人话就是——我漏了一次屏障转换。定位方法很简单:在命令列表里往前翻,找到这个资源上一次被转换的地方,看看中间有没有"另一个阶段的代码"把它改了状态。那次的原因是我在初始化纹理之后,把整个资源状态的批量转换写成了一个通用函数,而这个函数把这个顶点缓冲也一起转成了PIXEL_SHADER_RESOURCE。
这类问题有个通用的排查套路:打开 GPU-Based Validation,让运行时把每次违规访问的资源和期望状态都报出来,然后按"资源 → 状态历史"这条线找,比盲猜快十倍。
5.2 设备移除定位链路与 DRED 面包屑
设备移除(device removed)是 DX12 里最让人血压升高的一个:程序还在跑,突然所有命令失败,Present返回DXGI_ERROR_DEVICE_REMOVED,然后你调用GetDeviceRemovedReason得到一个十六进制错误码,看不出任何上下文。
这时候 DRED 就是救命稻草。它的配置同样要在创建设备之前完成:
ID3D12DeviceRemovedExtendedDataSettings* dredSettings; D3D12GetDebugInterface(IID_PPV_ARGS(&dredSettings)); dredSettings->SetAutoBreadcrumbsEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON); dredSettings->SetPageFaultEnablement(D3D12_DRED_ENABLEMENT_FORCED_ON);出问题之后,用ID3D12DeviceRemovedExtendedData取出两个输出:自动面包屑(GetAutoBreadcrumbsOutput)和页错误分配信息(GetPageFaultAllocationOutput)。面包屑会告诉你命令列表标签里最后一次打勾的位置,你就能定位到是哪个BeginEvent段落里挂掉的;页错误信息会告诉你那次非法访问落在了哪块资源上。
常见的三种成因:着色器里写了长循环触发超时(系统对单次 GPU 任务有时间限制)、顶点或索引越界访问、以及屏障写错导致的死锁——最后一种最阴险,因为 GPU 就是静静等着一个永远不来的状态。
5.3 退出时的 Live Object 报告
程序退出时如果 Debug Layer 打印一堆"资源仍然存在",那就是漏了释放。用ID3D12DebugDevice::ReportLiveDeviceObjects打开详细模式,它会按引用来源分类列出活对象。我踩过的几个典型泄漏点:
- 后备缓冲的
ComPtr在ResizeBuffers之后没释放干净; - 命令列表、命令队列、描述符堆在退出顺序上混乱,导致互相持有引用;
- 自己写的资源缓存用了裸指针,容器析构时没遍历释放。
有个小技巧:把设备对象包一层自己的 RAII 类,退出时先释放所有缓存,再释放描述符堆,最后释放设备和工厂。顺序错了也会报活对象,但那不是真的泄漏,只是释放时机不对,看报告时要留意。
5.4 PIX 和 RenderDoc 抓帧的正确姿势
这两个工具的分工我总结成一句话:PIX 更懂 DX12 的命令结构,RenderDoc 更懂资源和网格。
PIX 最有用的是资源状态历史视图和屏障的时间线,你能直观看到一次转换在哪个时间点发生、GPU 在那里等了多久。RenderDoc 的网格查看器和纹理查看器更顺手,采样点、mip 层级、通道都能单独看,调 UV 和格式问题时效率极高。
两个注意事项:第一,抓帧时关掉 GPU-Based Validation,因为它本身会引入巨大的性能偏差,你基于这个数据做性能判断会得出错误结论;第二,抓帧会改变任务调度时序,别拿抓帧里的帧时间当作真实帧时间,要测性能就用 GPU 时间戳查询自己在代码里埋点。
6. 报错速查与我的排错顺序
6.1 高频报错对照表
| 现象或报错 | 大概率原因 | 处理方向 |
|---|---|---|
DXGI_ERROR_INVALID_CALL出现在ResizeBuffers | 还有后备缓冲引用没释放 | 检查所有ComPtr与视图 |
| 绘制时报资源状态不匹配 | 漏写或写错屏障 | 查该资源的状态历史 |
| 提示描述符堆不可被着色器访问 | SRV 写进了非 shader-visible 堆 | 分堆处理 CBV/SRV/UAV |
E_INVALIDARG出现在创建资源 | 堆类型、格式、标志组合非法 | 逐字段核对组合是否被允许 |
| 画面全黑但无报错 | SRV 未绑定、屏障未转、UV 反向 | 先查绑定,再查状态,最后查 UV |
| 颜色整体发灰或偏暗 | 颜色贴图没设 sRGB 格式 | 区分颜色贴图与数据贴图 |
| 贴图出现斜向错位 | 上传时没按行拷贝 | 按 RowPitch 逐行复制 |
| 启动就提示不支持 DX12 | 驱动过旧、系统版本低或集显不支持目标特性级别 | 查设备创建返回值与特性支持 |
6.2 我在改代码时的固定检查顺序
踩了足够多的坑之后,我总结出一套固定的排查顺序,基本能覆盖八成问题:
第一,先确认 Debug Layer 是开着的,而且是在创建设备之前开的——很多人抱怨"没有报错",其实是调试层压根没生效。第二,永远只读第一条错误,后面的先忽略。第三,如果第一条错误涉及资源,就去查这块资源的状态历史,而不是去改着色器。第四,如果程序直接崩掉或者设备被移除,先看 DRED 的面包屑和页错误,再看是不是着色器里有长循环。第五,只有在前面都排干净之后,才去抓帧看具体画面。
这个顺序的核心逻辑是:从代价最低、信息量最大的检查开始。改着色器、改数学公式、重写渲染逻辑,都是代价高且容易引入新变量的操作,能放到最后就放到最后。
6.3 从贴图三角形往后能接什么
把带贴图的三角形画出来,其实只跨过了 DX12 的门槛,但它解锁的东西非常多。我的推荐路径是:先加常量缓冲和 MVP 矩阵,让三角形能转起来,这一步会逼你理解根常量和 256 字节对齐的常量缓冲;然后加深度缓冲,理解 DSV 和深度状态;再上 Instancing,理解实例数据和根参数;之后是纹理数组和描述符索引,这是走向 bindless 的必经之路;最后才是 Compute、后处理,以及光线追踪这类更重的内容。
每一层都只加一个新变量,出问题就能快速定位。我见过太多人一次上手写完整渲染器,结果卡在某个屏障上两周,最后把项目弃了——这不是能力问题,是方法问题。
最后分享一个我自己的习惯:我维护一个 Markdown 文件,专门记录"报错原始文本 → 真实原因 → 修复方式"。每次遇到新的设备移除或者状态冲突就补一条,现在这个文件已经成了我排查问题的第一入口。它比任何教程都有用,因为里面的每一条都是你自己踩出来的,而且写的时候你就已经在复盘了。贴图三角形画出来的那一刻其实不会有什么成就感,真正的成就感来自下一次看到红字时,你能在三分钟内说出"这是描述符堆用错了",然后改完继续往下走。