大多数浏览器和
Developer App 均支持流媒体播放。
-
将游戏移植到 Mac,第 3 部分:使用 Metal 进行渲染
这是关于将游戏引入 Mac 平台的系列讲座的最后一部分,了解如何在你的渲染代码中支持 Metal。在这个讲座中,将介绍如何在你的游戏渲染代码中添加对 Metal 的支持。一旦你通过游戏引入工具评估了现有的 Windows 二进制文件,并将 HLSL 着色器转换为 Metal 着色器,你可以学习如何最优化地实现高端、现代游戏所需的功能。本文还将向你展示如何管理 GPU 资源绑定、保持资源在 GPU 上的有效性和同步。你还将了解如何优化 GPU 命令的提交、使用 MetalFX Upscaling 渲染丰富的视觉效果等等。为了充分理解本次讲座,我们建议你首先观看“将你的游戏引入 Mac 平台,第 1 部分:制定游戏计划”和“将你的游戏引入 Mac 平台,第 2 部分:编译你的着色器”。
章节
- 0:00 - Intro
- 1:58 - Manage GPU resources
- 9:08 - Optimize rendering commands
- 18:00 - Handle indirect rendering
- 22:41 - Upscale with MetalFX
- 25:31 - Wrap-Up
资源
- Applying temporal antialiasing and upscaling using MetalFX
- Metal
- MetalFX
- Modern Rendering with Metal
相关视频
Tech Talks
WWDC23
WWDC22
WWDC21
WWDC20
-
下载
♪ ♪
Georgi:大家好 欢迎来到本次讲座! 我是 Georgi Rakidov 是 GPU、Graphics 和 Display Software 团队的工程师 本次讲座是关于 如何将游戏引入到 Mac 平台的 三部分系列内容的最后一部分 第一部分介绍了 如何使用全新的游戏引入工具包 在 Mac 上运行 未经修改的 Windows 游戏 以评估你的图形、音频和显示功能 第二部分展示了通过使用全新的 Metal Shader Converter 工具 将现有的 HLSL 着色器编译为 Metal 着色器 从而节省开发时间 而本次讲座将为你提供完整的指导 介绍如何 将渲染器引入到 Metal 并在 Apple 芯片上 获得卓越的性能表现 在将渲染器引入到 Metal 的过程中 你会发现 需要将其他平台的 图形 API 概念映射到 Metal 本次讲座包含四个主题 同时分享 Metal 最佳实践 帮助你充分利用 Apple GPU 强大的架构 在引入游戏时 每个游戏都需要 将 GPU 资源 如纹理和数据缓冲区 提供给 GPU 并配置着色器访问这些资源的方式 通过优化 向 GPU 提交命令的方式 你的游戏可以充分发挥 Apple 处理器强大的图形架构 游戏通常会采用 间接渲染技术实现现代渲染效果 而 MetalFX 可以帮助游戏以较低分辨率渲染 并通过 MetalFX 实现升采样 从而实现最终分辨率 在进行资源管理时 每个引擎都需要决定 GPU 访问各个 纹理、数据缓冲区等资源的方式 对于 Metal 重要的是 为着色器提供资源绑定 并将这些资源设置为 GPU 可访问内存中的驻留资源 同时保持对资源的访问同步 资源绑定和着色器是密切相关的 首先使用 Metal Shader Converter 将现有的着色器转换为 Metal 这是今年推出的一款新工具 可极大地节省 将着色器引入至 Metal 的时间 你可在本系列的 “编译你的着色器”讲座中 了解更多信息 Metal Shader Converter 提供了 两种绑定模型供你选择 使用“自动布局”时 转换器会自动生成绑定信息; 或者你可以选择“显式布局” 手动传递绑定信息给 Metal Shader Converter 显式布局非常灵活 特别适用于 需要部署来自 其他平台的绑定模型的情况 例如 某些 API 设计 采用了着色器根签名 下面是一个典型的 根签名结构 包含四个条目: 一个描述器表 指向一系列纹理; 一个缓冲区根参数; 一个 32 位常量; 以及另一个描述器表 指向一系列采样器 每个描述器表都是 一个包含相同类型元素的资源数组 例如全部纹理、 全部采样器或全部缓冲区 因为元素可以是多种类型 Metal 的参数缓冲区更加灵活 但是 如果你的引擎 需要的是同类型的数组 你可以通过参数缓冲区 轻松地对它们进行编码 这个示例编码了 一个等效的纹理描述器表 它首先通过 存储每个纹理的 Metal resourceID 分配一个 Metal 缓冲区 用作纹理描述器表 当它创建每个纹理时 代码直接 将其 resourceID 存储到表中 好处是 你可以事先运行此类代码 不需要在渲染循环中运行它! 对采样器描述器表 进行编译的过程与此几乎相同 就像纹理一样 代码首先创建一个 用作采样器 描述器表的 Metal 缓冲区 随后 代码配置每个采样器的描述器 将 supportArgumentBuffers 属性设置为 YES 在代码使用描述器创建采样器之后 它将采样器的 resourceID 保存在表中 你还可以使用参数缓冲区 来表示顶层根签名本身 这个示例定义了 一个用于根签名的结构 并创建一个可以存储一个 结构实例的 Metal 缓冲区 这段代码为结构字段的每个字段 分配适当的值 包括纹理和 采样器表的 GPU 地址 这样 就可以将根签名 转换为参数缓冲区了 在 Metal 3 中 参数缓冲区非常高效! 现在 你只需将 顶层参数缓冲区绑定到着色器中 这部分是在渲染循环中完成的 但是你可以在渲染循环之外 预先创建描述器表和根结构 Metal 3 的参数缓冲区 为转换其他绑定模型 包括根签名和描述器表 提供了灵活且高性能的方式 在渲染期间 资源需要始终存在于 给定的传递或渲染阶段中 以便着色器可以访问它们 如果一个资源 在多个 Pass 之间共享 那么这些 Pass 的 执行顺序必须进行同步 使用 Metal 参数缓冲区的 无绑定资源 需要在所有 GPU 架构上 进行显式的驻留管理 而 Metal 提供了 有效的方法来控制驻留 建议将所有只读资源分组到大堆中 这样 你只需在 每个编码器中调用 useHeap 一次 所有只读资源就都会在该传递 或渲染阶段期间驻留 准备供着色器访问 接下来我将向你说明操作方法 创建一个具有 足够大小的堆来分配所有只读资源 然后从这个堆中分配每个资源 在渲染时 只需调用 useHeap 以使所有这些资源保持驻留状态 对于可写资源 情况有所不同 考虑逐个分配可写资源 并使用正确的使用标志 调用 useResource 在这种情况下 Metal 将为你处理同步 并优化性能 这将帮助你避免 在 Metal 编码器之间 手动同步资源的负担 与之前类似 首先分配资源 但这次不是由堆支持 然后 仅对将访问 这些资源的编码器调用 带有正确使用标志的 useResource 在这个示例中 编码器写入纹理并从缓冲区中读取 下面是这个建议的表格 只读资源和可写资源 都从顶层参数缓冲区访问 在理想情况下 每个编码器只需设置一次 只读资源以堆的形式分组 危险追踪模式设置为未跟踪 要使堆中的所有资源保持驻留状态 请每个编码器 调用一次 useHeap 可写资源逐个分配 将危险追踪和同步 交给 Metal 处理 对于每个资源调用 useResource 每个编码器调用一次 这种方法很高效! 这种方法可以实现无绑定模型 并且有着较低的 CPU 额外开销 无需担心 App 的 危险追踪和同步 减少了开发人员在 这些复杂任务上投入的时间和专注 要了解有关无绑定、 资源驻留和同步的更多详细信息 请查看讲座 “使用 Metal 3 进行无绑定” 一旦你在代码中实现了 资源绑定、驻留和同步 要在屏幕上渲染任何内容 引擎都必须向渲染器发送命令 Apple 处理器 具有许多优化命令执行的特性 其 GPU 是一种 基于图块的延迟渲染器 也即 TBDR 具有统一内存架构 其中 CPU 和 GPU 共享系统内存 此外 GPU 还有一个 快速的片上内存 称为图块内存 为了利用这种架构 Metal 引入了 Pass 的概念 你需要将渲染命令 分组到 Pass 中 并正确配置这些 Pass 有关 TBDR 架构的更深入介绍 请查看以下讲座 “将你的 Metal App 带到搭载 Apple 芯片的 Mac 上” 以及“利用 Metal 发挥 Apple GPU 的潜力” 其他 API 可能会一直传输 不同类型的 GPU 命令 而你的引擎可能会有如此假设 将命令转换为 Metal 时 首先创建一个命令缓冲区 然后 根据命令的类型 Graphics、Compute 或 Blit 将它们分组为 Pass 使用命令编码器 将每个 Pass 的命令 写入命令缓冲区 最后 当所有命令都编码完成后 将命令缓冲区提交给 GPU 的命令队列以进行执行 你的引擎 可以考虑以下四个最佳实践 以有效地 将渲染命令转换为 Metal 首先 在渲染开始之前 批处理进行复制操作 然后 将相同类型的命令进行分组 同时 清除空编码器 以清除渲染目标 最后 优化 Metal 的 加载和存储操作 以减少内存带宽的使用 这些最佳实践 可以通过一个示例来更好地解释 假设你有以下序列: 一个渲染目标清除操作 一次绘制操作 一次复制操作 一次调度操作和另一次绘制操作 特别注意在这个序列中 产生的系统 和图块内存之间的所有内存传输 这并不是理想的情况! 中间的复制操作 用于复制后续绘制操作的统一数据 在本例当中 是 Draw 1 建议的做法是 如果可能的话 在渲染开始之前 移动和批处理这些复制操作 以避免中断渲染 Pass 经过修改后 现在复制操作位于第一位 然后是清除操作、 Draw 0、调度操作和 Draw 1 如果两次绘制 和调度之间没有依赖关系 应该对它们进行重新排序 以便可以批处理进行绘制和调度 在这个例子中 通过交换绘制和调度调用的顺序 现在你可以 连续进行两个渲染 Pass 在此情境下 如果这两个 Pass 共享相同的渲染目标 那么就很适合将它们 合并为一个渲染 Pass 从而节省大量的内存带宽 这样 可以去除 一些不必要的内存传输 因为在两次绘制之间 数据无需 在图块内存 和系统内存之间来回传输 已经有所改善了 但还可以进一步优化 清除操作是一个 空的编码器 只有一个目的: 清除下一个绘制所使用的渲染目标 在 Metal 中 有一种 非常高效的方法可以做到这一点 只需使用 LoadActionClear 来设置第一个使用 渲染目标的渲染通道 这样就好多了 但还有一个建议 你可以优化加载和存储操作 你只需在系统内存中 存储下一个 Pass 中 将要使用的渲染目标的内容 以这个例子为例 在 Draw 1 之后 只有第一个渲染目标会被使用 其他的渲染目标都是中间结果 不需要保留其内容 Metal 允许对 每个渲染目标进行存储操作的控制 在这种情况下 你可以将 StoreActionStore 应用于第一个渲染目标 将 StoreActionDontCare 应用于其他的渲染目标 完成了!这是初始的指令序列 在这个序列中 在图块内存 和系统内存之间有五次往返 通过一些简单的优化 指令序列变得如下所示 只需进行一次最后的刷新操作 从图块内存到 系统内存的内存带宽大大降低! 这是通过 在将拷贝操作移动到渲染之前 对相同类型的指令进行分组 避免使用空编码器 进行渲染目标的清除操作 以及优化加载和存储操作来实现的 GPU 工具 可以帮助你识别这些问题 在 Xcode 中 Metal 调试器会自动发现优化机会 让你的游戏性能达到最佳状态 它让你可以检查和理解 Metal Pass 之间的依赖关系 并提供了 一套功能齐全的调试和分析工具 使用 Metal 调试器 来识别上述提到的问题非常简单 当我捕获 Metal 工作负载时 Metal 调试器会显示摘要视图 底部的洞察部分 显示了针对四个类别的优化机会 包括内存、带宽、 性能和 API 使用 在这个工作负载中 有两个带宽洞察值得关注 第一个是关于未使用资源的 当我选择一个洞察时 在右侧的面板上 会显示该洞察的摘要和一些建议 GBuffer Pass 存储了 比它实际所需更多的附件 在本例中 GBuffer Pass 加载了反射率/透明度纹理 并将其存储起来 然而 由于反射率纹理在此帧后 不再使用 所以存储操作是多余的 我们可以通过将存储操作设置为 DontCare 来修复这个问题 让我们来检查下一个洞察 合并渲染 Pass 可以帮助减少带宽 这里的洞察建议我 可以将 GBuffer Pass 和 Forward Pass 合并为一个 我还可以通过点击 右侧的“显示依赖项”按钮 来了解这些 Pass 的 读取和写入情况 在依赖项查看器中 找到这个渲染 Pass 依赖项查看器是一个很棒的工具 可以检查 Pass 之间的依赖关系! 在这里 我可以一目了然地 看到显示在渲染附件的上下方的 加载和存储操作 在这个 Pass 中 所有的附件都具有存储操作 但只有 color 0 和深度附件在后续 Pass 中会使用 前面的洞察也表明了这一点 稍微缩放 可以看到数据边缘 从 GBuffer Pass 流向 Forward Pass 正如洞察所指出的 GBuffer 和 Forward Pass 可以合并以节省带宽 因为它们都在使用 相同的附件进行存储和加载 合并这两个 Pass 将能节省带宽并提高性能 这只是使用 Metal Debugger 找到游戏中优化机会的一个例子 想进一步了解 Metal 调试器的信息 请查看讲座“使用 Xcode 12 获得关于 Metal App 的洞察” 和“发现 Metal 调试、 性能分析和资产创建工具” 间接渲染是高端游戏 实现高级渲染技术的重要功能 本次讲座将介绍 ExecuteIndirect 的工作原理 以及如何将 此特定命令转换为 Metal 通过使用间接渲染 我们不再编码多个绘制命令 而是将其参数存储在 内存中的常规缓冲区中 并仅编码一个 ExecuteIndirect 命令 引用该缓冲区并指定 GPU 需要执行的绘制调用数量 并从缓冲区中 提取每个绘制调用的参数 这种方法的中心思想是 通过在执行 ExecuteIndirect 命令之前 调度计算着色器 来填充间接缓冲区的内容 这样做 GPU 可以为自身准备工作 并决定要渲染的内容 使用间接参数执行命令 是实现高级技术的关键特性 如基于 GPU 的渲染循环 有两种方式可以 将这个命令转换为 Metal 语言 通过使用 Draw Indirect 和 Metal Indirect Command Buffers (ICB) 在 Metal 中 渲染器需要 将每个 ExecuteIndirect 命令 转换为一系列 DrawIndirect 的 API 调用 每个调用都引用缓冲区 并提供绘制参数的偏移量 下面是代码示例 检查 ExecuteIndirect 可能具有的最大绘制调用次数 对于每个调用 编码一个单独的绘制命令 指定间接参数缓冲区 和缓冲区中的偏移量 在迭代结束时 将偏移量 移动到指向下一组间接参数 这种方法非常容易实现 并且 在几乎所有情况下都能正常工作 然而 如果你的场景中 有成千上万个绘制调用 且游戏性能 受到 CPU 编码时间的限制 你应该考虑使用 Metal 中的 Indirect Command Buffers (ICB) ICB 是带有 间接绘制参数的缓冲区的超集 除了绘制参数之外 还可以 从 GPU 设置缓冲区绑定 和渲染管线状态对象 要将 ICB 中的命令 安排在 GPU 上执行 需要编码 executeCommandsInBuffer 命令 通常情况下 使用 ExecuteIndirect 时 所有绘制调用共享相同的 管线状态对象 (PSO) 每次 PSO 发生更改时 都需要编码新的 ExecuteIndirect 命令 如果使用 ICB 则不需要经常通过状态更改 来拆分间接执行命令 所有的 PSO 和 缓冲区绑定都可以从 ICB 中设置 因此不需要进行编码 这可能会显著减少编码时间 具体取决于场景的结构 要利用 ICB 并不需要修改 现有的填充间接参数的着色器 你可与其他平台共享相同的着色器 并使用 Metal Shader Converter 进行编译 然后添加一个小的计算内核 以将绘制参数转换为 ICB 以在间接参数生成之后 和间接渲染 Pass 之前进行 为了在计算内核中编码 ICB 使用 Metal 着色器语言进行编写 作为着色器的输入 有一个指向 你想要转换的间接参数的指针 接下来 检查参数是否有效 只有在有效时才会编码命令 在 encodeCommand 函数中 设置渲染管线状态、 缓冲区绑定和绘制调用 这将把绘制参数转换为 间接命令缓冲区中的渲染命令 这就是如何 将间接渲染转换为 Metal 语言 你可以使用一系列的绘制间接命令 或 Metal Indirect Command Buffers 如果你想要学习如何利用间接渲染 来实现高级渲染技术 请查看 “Modern Rendering with Metal” 示例代码 一旦你的游戏 通过将资源绑定到其管线 并正确编码命令到 命令缓冲区 生成了正确的图像 你可以利用采样来提高 其在玩家设备上的性能 通过 MetalFX 进行上采样 有助于游戏节省每帧的时间 减少 GPU 的工作量 MetalFX 是一个即插即用的 解决方案 用于实现上采样管线 它通过以比直接渲染 输出分辨率更快的时间 将较低分辨率图像 缩放到目标输出分辨率 MetalFX 在去年 引入 Mac 平台 提供了高性能的上采样能力! MetalFX 支持 两种上采样算法 其中 “Spatial”算法 具有最佳性能 而“Temporal”算法 接近原生渲染的质量 MetalFX 的集成 将以更高的分辨率 和更好的 性能进行渲染 提升玩家的体验 今年的新功能包括 对 iOS 的支持 最高可达 3 倍的上采样 并且在 Metal-cpp 中提供支持
如果你的引擎已经支持其他平台上的 现有上采样解决方案 集成 MetalFX 不需要太多的编码 和修改引擎 要支持 MetalFX 你需要在引擎中支持上采样 另一个要求是渲染器能够手动控制 材质着色器中纹理采样的细节级别 Temporal 上采样需要 jitter 序列和运动向量 如果你的引擎支持时间抗锯齿 则你可能已经具备了这些特性 MetalFX 的 Temporal 上采样可以考虑渲染曝光 而且你有两个选择 如果你的渲染器支持 1x1 曝光纹理 那么就使用该功能 否则 你可以启用自动曝光功能 看看是否能提高质量 不要忘记在摄像机切换 和摄像机剧烈移动时重置历史 要了解如何将 MetalFX 集成到你的 App 中的更多细节 请参考文档 和去年的“使用 MetalFX 上采样提升性能” Metal 提供了一些 强大的选项 可以最大程度地 利用 App 的渲染时间 你可以尽可能地 高效管理和绑定资源 根据着色器访问资源的方式 确保共享资源的 Pass 以正确的顺序运行 并将资源保留并提供给 GPU 通过在 Xcode 中使用 Metal Debugger 并优化命令提交 你的 App 可以充分发挥 Apple 强大的图形架构潜力 通过实现间接渲染 让 GPU 自行决定要执行的工作 这是许多现代渲染技术的关键所在 通过 MetalFX 的渲染上采样 来提升你的渲染效果 可以在渲染循环中节省宝贵的时间 如欲了解更多渲染技巧和指南 请查看“为搭载 Apple 芯片的 Mac 优化 Metal 性能” 感谢观看! ♪ ♪
-
-
3:55 - Encode the texture tables.
// Encode the texture tables outside of the rendering loop. id<MTLBuffer> textureTable = [device newBufferWithLength:sizeof(MTLResourceID) * texturesCount options:MTLResourceStorageModeShared]; MTLResourceID* textureTableCPUPtr = (MTLResourceID*)textureTable.contents; for (uint32_t i = 0; i < texturesCount; ++i) { // create the textures. id<MTLTexture> texture = [device newTextureWithDescriptor:textureDesc[i]]; // encode texture in argument buffer textureTableCPUPtr[i] = texture.gpuResourceID; }
-
4:33 - Encode the sampler tables.
// Encode the sampler tables outside of the rendering loop. id<MTLBuffer> samplerTable = [device newBufferWithLength:sizeof(MTLResourceID) * samplersCount options:MTLResourceStorageModeShared]; MTLResourceID* samplerTableCPUPtr = (MTLResourceID*)samplerTable.contents; for (uint32_t i = 0; i < samplersCount; ++i) { // create sampler descriptor MTLSamplerDescriptor* desc = [MTLSamplerDescriptor new]; desc.supportArgumentBuffers = YES; . . . // create a sampler id<MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc]; // encode the sampler in argument buffer samplerTableCPUPtr[i] = sampler.gpuResourceID; }
-
5:05 - Encode the top level argument buffer.
// Encode the top level argument buffer. struct TopLevelAB { MTLResourceID* textureTable; float* myBuffer; uint32_t myConstant; MTLResourceID* samplerTable; }; id<MTLBuffer> topAB = [device newBufferWithLength:sizeof(TopLevelAB) options:MTLResourceStorageModeShared]; TopLevelAB* topABCPUPtr = (TopLevelAB*)topAB.contents; topABCPUPtr->textureTable = (MTLResourceID*)textureTable.gpuAddress; topABCPUPtr->myBuffer = (float*)myBuffer.gpuAddress; topABCPUPtr->myConstant = 128; topABCPUPtr->samplerTable = (MTLResourceID*)samplerTable.gpuAddress;
-
6:49 - Allocate the read-only resources.
// Allocate the read-only resources from a heap. MTLHeapDescriptor* heapDesc = [MTLHeapDescriptor new]; heapDesc.size = requiredSize; heapDesc.type = MTLHeapTypeAutomatic; id<MTLHeap> heap = [device newHeapWithDescriptor:heapDesc]; // Allocate the textures and the buffers from the heap. id<MTLTexture> texture = [heap newTextureWithDescriptor:desc]; id<MTLBuffer> buffer = [heap newBufferWithLength:length options:options]; . . . // Make the heap resident once for each encoder that uses it. [encoder useHeap:heap];
-
7:34 - Allocate the writable resources.
// Allocate the writable resources individually. id<MTLTexture> textureRW = [device newTextureWithDescriptor:desc]; id<MTLBuffer> bufferRW = [device newBufferWithLength:length options:options]; // Mark these resources resident when they're needed in the current encoder. // Specify the resource usage in the encoder using MTLResourceUsage. [encoder useResource:textureRW usage:MTLResourceUsageWrite stages:stage]; [encoder useResource:bufferRW usage:MTLResourceUsageRead stages:stage];
-
19:31 - Encode the execute indirect
// Encode the execute indirect command as a series of indirect draw calls. for (uint32_t i = 0; i < maxDrawCount; ++i) { // Encode the current indirect draw call. [renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle indexType:MTLIndexTypeUInt16 indexBuffer:indexBuffer indexBufferOffset:indexBufferOffset indirectBuffer:drawArgumentsBuffer indirectBufferOffset:drawArgumentsBufferOffset]; // Advance the draw arguments buffer offset to the next indirect arguments. drawArgumentsBufferOffset += sizeof(MTLDrawIndexedPrimitivesIndirectArguments); }
-
21:48 - Translate the indirect draw arguments to ICB.
// Kernel written in Metal Shading Language to translate the indirect draw arguments to an ICB. kernel void translateToICB(device const Command* indirectCommands [[ buffer(0) ]], device const ICBContainerAB* icb [[ buffer(1) ]], . . .) { . . . device const Command* indirectCommand = &indirectCommands[commandIndex]; device const MTLDrawIndexedPrimitivesIndirectArguments* args = &command->mdiBuffer[mdiIndex]; render_command drawCall(icb->buffer, indirectCommand->mdiCmdStart + mdiIndex); if(args->indexCount > 0 && args->instanceCount > 0) { encodeCommand(indirectCommand, args, drawCall); } else { cmd.reset(); } } // Encode a render command on the GPU. void encodeCommand(device const Command* indirectCommand, device const MTLDrawIndexedPrimitivesIndirectArguments* args, thread render_command& drawCall) { drawCall.set_render_pipeline_state(indirectCommand->pso); for(ushort i = 0; i < indirectCommand->vertexBuffersCount; ++i) { drawCall.set_vertex_buffer(indirectCommand->vertexBuffer[i].buffer, indirectCommand->vertexBuffer[i].slot); } for(ushort i = 0; i < indirectCommand->fragmentBuffersCount; ++i) { drawCall.set_fragment_buffer(indirectCommand->fragmentBuffer[i].buffer, indirectCommand->fragmentBuffer[i].slot); } drawCall.draw_indexed_primitives(primitive_type::triangle, args->indexCount, indirectCommand->indexBuffer + args->indexStart, args->instanceCount, args->baseVertex, args->baseInstance); }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。