找回密码
 立即注册
查看: 274|回复: 0

GPU渲染管线和架构整理(一)

[复制链接]
发表于 2022-11-29 13:28 | 显示全部楼层 |阅读模式
前言


本文是对腾讯技术工程号官方文章《GPU 渲染管线和硬件架构浅谈》的一点浅显的学习笔记,主要工作是对庞大的内容进行了梳理和知识点归类,增补了部分技术资料,追加了一些个人批注,以便于读者更好的理解文章内容。作为文章的第一部分,内容主要集中在渲染管线的概要,桌面端和移动端管线的主要区别,以及桌面端GPU的架构梳理上。好了废话不多说,直接开始吧!
一:简述渲染管线

1)应用阶段(CPU端的事情)

    主要用于粗粒度剔除,渲染状态设置(SetPass),然后为GPU准备必要的数据(模型,材质等)
  • 所谓“粗粒度剔除”
      游戏引擎代码里做的“视椎剔除”(比如GPUInstance接口中帮我们执行了视椎剔除),遮挡剔除(OC)都是这类粗粒度的活(模型级别,基于包围盒/层级包围盒的剔除)

2)整个顶点处理阶段

    这里面包括了“顶点着色器”->“曲面细分”->“几何着色器”-> “顶点裁剪”-> “屏幕映射”顶点的裁剪不光包括了精细粒度的视椎裁剪,也包括可能的“背面剔除”或“正面剔除”
  • 可编程的部分:
      Vertex Shader (顶点着色)Tessellation Control Shader (曲面细分控制着色)Geometry Shader (几何着色)

3)光栅化阶段

    “三角形设置” -> “三角形遍历” -> “片元着色器” -> “逐片元操作”以上步骤组成了广义上的“光栅化”(Rasterization) 阶段将图元“Primitive”映射为屏幕像素对应的片元“Fragment”在DirectX图形API中叫做像素着色器“Pixel Shader”可编程部分比较少:Fragment/Pixel Shader
  • 大多数情况下,性能瓶颈出现在这里(FS或PS)!
      利用诸如Early-Z,隐面剔除等技术 -> 用于避免执行无效像素处理逻辑 -> 从而优化Fragment Shader

4)逐片元

    “裁剪测试”ClipTest,“深度测试”Z-Test,“模板测试”StencilTest,“混合”Blend光栅化阶段得到的颜色(注意是颜色,意味着已经过了“片元着色器”了)会经过如上的一系列测试,混合,最终写入到FrameBuffer中
  • “Raster Operations”ROP -> 由一个独立的硬件单元完成,它的强弱影响GPU每秒写入Framebuffer的数据量,可能成为瓶颈
      注意:ROP不是光栅化器!!!它处理深度测试和颜色混合等操作,它负责颜色的原子写入,它对从属于一个像素的多个颜色进行排序(按照API调用先后排)
    上述问题案例 -> 低端机上多叠加几次全屏渲染就会严重降帧
二:经典管线“Immediate Mode Rendering”IMR


IMR

以上是桌面端目前(2022)经典管线,每一个GPU的DrawCall提交,都会按照上面这个顺序执行和处理。
1)IMR的优势

    单次DC直接出最终颜色结果,期间无中断!这是有利于提高GPU吞吐量,最大化GPU性能的。从Vertex到Raster期间,所有处理都在GPU内部的 on-chip buffer 上进行 -> 节省带宽 bandwidth 啊!!这就是为什么桌面端GPU天然适合处理大量DrawCall和海量Vertex移动端对DC数量和Vertex数量非常敏感 -> 引出他们之间迥异的架构
2)IMR的劣势

    每个DC绘制一个或多个图元(primitive),这些图元可能分布在屏幕上任何地方,因此必须位置全屏尺寸的 framebuffer受屏幕分辨率和颜色和深度存储格式的影响,framebuffer的大小可能非常大,因此无法做到on-chip,只能放在系统内存中shading过程中(主要是fragment shading, depth testing, stencil testing,blending)会大量读写数据到on-chip,占用带宽GPU一般用 L1/L2缓存来缓解问题,但不足以解决移动端的瓶颈
三:移动端管线“Tile-Base Rendering”TBR


TBR

1)由来:

    数据带宽是影响功耗的最大因素(第一杀手),而功耗在移动端非常重要,因为高功耗导致耗电,发热和降频移动端带宽从架构到体量上都不及桌面端,而且GPU的带宽也不是独占的 -> 更加捉襟见肘
2)TBR架构原理(简略)

    与IMR不同,不是基于Full-Screen绘制,而是基于一个个小块 Tile 进行。
  • GPU 在绘制完一个Tile之后,再把结果写入到系统内存的 FrameBuffer (全屏的)中去

    • 绘制第一步:处理所有输入的顶点(此时已经完成了Vertex Shader),生成中间数据(FrameData)
        [图元A] -> [Tile_0][图元B] -> [Tile_12, Tile_921] ...
      绘制第二步:逐Tile执行光栅化 + 片元着色,最后将结果写入系统内存中
    Tile 大小一般为 32X32
3)TBR的优势


  • 最大优势 -> 减少对主存的访问,对带宽的依赖:只要Tile足够小,framebuffer可以做到on-chip!
      framebuffer on chip的好处:frag shader,depth\stencil testing,blending等操作都on-chip注意这是与IMR不同的,IMR只有从Vertex到Raster阶段是在on-chip buffer上进行
    depth\stencil buffer将不再必须写回framebuffer对一些依赖大量局部数据读取的算法(如MSAA),会提高效率(因为都可以在on-chip上采)
4)TBR的劣势

    GPU必须对所有顶点进行额外处理 -> 生成中间数据 FrameData
  • 这些存放了tile-list的FrameData本身被存放在了系统内存中,对它们的访问会产生带宽开销
      可见顶点越多,计算和存储压力越大诸如曲面细分(Tessellation)或海量顶点数据 -> 对TBR来说都是昂贵滴!

四:移动端管线“Tile-Base Deffered Rendering”TBDR


TBDR

说明:

    与TBR相比,在Tile上光栅化后并不是立即渲染,而是多了一个隐面剔除(Hidden Sruface Removal)过程。无论以什么顺序提交的DrawCall,经过HSR处理(过滤)后,最终只有对屏幕产生贡献的像素会执行片元着色器所谓TBDR中的D(Deffered):尽可能延迟执行Fragment Shader,直到所有光栅化后的fragment完成了DepthTest和HSR
  • 对于全是Opequa(不透明)材质的场景,每一个图元在光栅化后都会经历HSR和DepthTest,最后只留下能对屏幕产生影响的frag
      确保了每一个像素只执行一次pixel shader(不考虑MSAA)

  • 对于以下集中情况
      Alpha Test:必须执行Frag Shader才能算出当前Fragment是否需要被丢弃Alpha Blend:总是要执行Frag Shader,同时还要存储和读取在FrameBuffer中的当前像素颜色记录Pixel Depth Write:不可不执行,因为它的Frag Shader输出的"手K深度"会影响后续Fragment的HSR以及DepthTest
    简言之,以上3种情况都会打破一个像素只执行一次Pixel Shader的设计,打乱原先deffered的流程,打断HSR
五:CPU与GPU的基本硬件差异


CPUvsGPU

1)简要总览


  • CPU核心(计算单元)少,每一个Core都有控制器(Controler)
      GPU核心(计算单元)非常多,多个Core才共享一套控制器(Controler)

  • CPU内存设计要求:很大的缓存(Cache),多级缓存以尽可能降低延迟
      GPU内存设计追求:很高的吞吐量(高带宽),可以接受较高的延迟

  • CPU善于分支控制,复杂逻辑运算
      GPU不善于分支控制和复杂逻辑运算(代价高昂) -> 归咎于缺少控制器,单个运算单元体量小等直接因素

  • GPU善于海量数据的并行计算场景
      CPU的运算核心虽然强大,但是数量不够多

2)CPU的缓存和指令


  • 内存分类

    • SRAM(Static Random Access Memory),相对于动态而言无需刷新电路即可保存数据,比DRAM快许多
        用于 on-chip cache -> L1/L2

    • DRAM(Dynamic Random Access Memory),需要不停刷新电路以维持数据存储,容量可做大,但速度较慢
        用于系统内存 System MemoryDDR SDRAM(Double Data Rate, Synchronous Dynamic Random Access Memory)

    • GDDR(Graphic DDR),时钟频率更高(相比于DDR),同时耗电量更少
        常用于桌面端显存
      LPDDR SDRAM(Low Power Double Date Rate),低功耗的SDRAM移动设备常用该类型SDRAM作为系统内存和显存FrameBuffer存放于此

  • 缓存体系
      L3 -> L2 -> L1,L1和L2位于Core内部独占,L3则被全部Core共享L1,L2和L3全部都是 SRAM
    • L1 和 L2 作为on-core cache,通常比较小,在几百KB量级
        做大L1和L2会导致增加访问时钟周期,目前的尺寸是平衡了Cache missing和Access Cycle的结果L1和L2需要处理缓存一致性问题,因为描述同一件事物的数据,如果分布在不同Core,就会涉及这一问题对 L1 Cache 而言,还能分为指令缓存(I-Cache)和数据缓存(D-Cache)有的架构 L2 会被几个Core共享
      L3 cache可达数十到数百MB量级,视为System Level Cache访问/获取数据的流程:L1 ->  L2 -> L3 -> SyetemMemory

3)CPU的指令执行过程


  • 经典的指令流水线如下5个阶段

    • Instruction Fetch,取指令
        指令从I-Cache中取出,放到指令寄存器里

    • Instruction Decode,指令解码
        此过程也包含了取得指令的源操作数(直白说就是计算的入参),这些src operand存放在寄存器文件中(Register File)
      Execute,执行指令
    • Memory Access,从存储单元存或取数据(load/store指令)
        只有涉及数据访问的指令才会触发Memory Access最先访问的是数据缓存(D-Cache)
      Register Write Back,将执行指令后所得的结果写入目的寄存器中

  • CPU的内存访问延迟遮掩技术

    • 分支预测:提前判断挑战逻辑的走向,现代CPU的分支预测成功率在90%左右
        作为对比,GPU完全木有分支预测能力

    • 超标量设计(Super Scalar):CPU也能同时发射多条指令,让指令“并行计算”
        其本质是让指令流水线负责5个阶段的逻辑单元尽量别停下来
      乱序执行(Out-of-Order):避免频繁出现高延迟指令,CPU在确保正确的基础上会修改指令执行顺序
    • 超线程:本质是切换上下文,一个Core准备两套寄存器,当遇到长延迟指令时可以非常低成本得切换线程
        作为对比,GPU由于拥有众多寄存器,上下文切换可谓是零成本的


4)GPU的硬件之于Rendering Pipeline

    应用层通过图像API(DirectX,OpenGL,Vulkan,Metal等)发送渲染命令,GPU通过驱动程序接收
  • 流处理器SM(Streaming Multiprocessor)处理顶点着色
      SM作为“统一着色器架构”(Unified Shader Architecture),处理顶点和像素着色任务

  • 固定后的三角形(此后三角形的位置不再变化)会被裁剪和剔除
      这部分由专门硬件支持完成

  • 过滤后的三角形会分配给光栅化引擎

    • 光栅化阶段会把三角形离散为与屏幕对应的格栅信息
        几何上连续的三角形 -> 由马赛克像素构成的一系列格栅以及附加其上的信息


  • 光栅化后 -> 得到“片元”
      此时可能有 Early-Z,用于过滤不可见的像素
    • 生成“像素/片元线程”,一般32个线程算作一个线程束(Warp)
        一个Warp是GPU计算(指令)核心的最小工作单元


  • 流处理器SM(Streaming Multiprocessor)处理片元着色
      同一个Warp中执行的指令完全一样同一个Warp中每个线程处理的数据不一样SIMD/SIMT (Single Instruction Multiple Data)

  • ROP(Raster Operation)接手流处理器输出
      一个ROP内部有很多ROP处理单元
    • 处理
        深度测试Blending(和FrameBuffer混合)TODO: 是否涉及 裁剪测试,模板测试??
      深度测试和颜色写入必须是原子操作,否则2个不同三角形在同一个像素点有可能会有冲突和错误

六:GPU的硬件(桌面端)


Nvidias-Fermi-GPU-Architecture_A


Nvidias-Fermi-GPU-Architecture_B

1)主要结构与核心组件(参考上图Fermi架构)


  • Fermi SM(上左图中),既 Streaming Multiprocessor
      GPU核心,执行Shader指令的地方(可编程部分)Mali中类似的单元角“Shader Core”,PowerVR中叫做“Unified Shading Cluster”

  • Core(上右图中)是真正执行指令的地方
      Navida管Core叫做 CUDA CoreMali中叫做 Execution CorePowerVR叫做Pipeline

  • Raster Engine(下图)光栅化引擎
      由若干个SM共享

  • ROP(图中无)
      Depth Testing、Blending操作在此完成
    Register File是寄存器,L1,L2 Cache是缓存
2)GPU中的CUDA Core(或叫Shader Core)

    32个CUDA Core构成一组SM一组SM拥有16套 LD/ST(Load/Store)模块来加载和存储数据一组SM有4个SFU (Special Function Unit)用于特殊数学运算(sin,cos,ln等)
  • 一组SM共享128KB寄存器(Register File)
      3万个 32-bit的寄存器 -> 大寄存器设计

  • 一组SM共享64KB L1缓存
      On-Chip Memory非常快
    一组SM有唯一的一个指令缓存(Instruction Cache)
  • 一组SM有多组线程束调度器(Warp Schedulers)
      负责Warp调度,已知一个Warp由32线程组成调度器指令通过 Dispatch Unit 传达到 Core 执行
    一组SM拥有(?)个纹理读取单元(Texture Unit)
3)GPU内存结构


GPU_VRAM


  • DMA 和 UMA
      DMA -> 桌面端为主UMA -> 移动端,以及桌面端中的集显

  • 关于UMA
      UMA适应移动端 -> Soc(System on Chip)
    • UMA并不意味着CPU和GPU内存数据是直接互通的
        GPU有自己管理的专有内存区域,CPU传递数据到GPU任然需要拷贝通过MapBuffer进行拷贝
      • 拷贝的理由是:
          CPU和GPU对待数据格式不同,GPU需要专门优化过的内存布局拷贝过程中同步优化了内存如果不拷贝,那GPU读取的就是CPU布局的内存,优化不佳时性能会下降


    • UMA在主机平台/苹果M系列芯片上能够做到 0-Copy 传输数据到GPU
        主要还是得益于CPU和GPU内存布局的统一

    • UMA使用同一份物理内存
        因为是同一份物理内存 -> CPU和GPU存在抢带宽的情况


  • GPU使用独显(或进行内存拷贝)的好处
      更加合适的内存布局/频率/延迟等对Buffer和Texture等数据的存储进行优化

4)GPU缓存分类


GPU Cache Architecture


  • L1 Cache:on-chip cache -> 片上缓存
      每一组SM(或Shader Core)都有独立的 L1 片上缓存速度很快

  • TileMemory:on-chip memory -> 片上内存
      移动端特有叫法参考 SMEM

  • SMEM: on-chip  Shared MEMory
      一组SM(Shader Core)内共享
    • 和 L1是同一套物理硬件单元
        区别:Shared Memory 可由开发者控制,L1则完全由GPU控制相同:L1、SMEM、TileMemory被SM中的所有CudaCore(ExecutionCore)共享,速度很快Mali没有SMEM -> 只有TileMemory


  • L2 Cache:Shared Cache
      由全部SM(Shader Core)共享,距离稍远,速度稍慢对DRAM的访问需要经过L2

  • DRAM:系统内存(System Memory)
      Global Memory / System Memory / Device Memory -> 全局内存相对访问速度最慢FrameBuffer就放在其上

  • Constant memory:常量内存
      on-chip 部分

      主要是为了提高常量命中需要在launch kernel前配置好
      off-chip 部分

      Global Memory(主存)上的一块区域全局作用域,既所有kernel都能访问

  • Texture/Const/Local Memory
      Global Memory(主存)上的一块区域访问速度慢注意constant memory在有的架构上是有on-chip部分的
    • Local Memory 是每个线程私有的
        主要为了处理寄存器溢出(Register spilling)或者用于存放超大的 uniform 数组再次强调,由于是主存的一部分,访问速度贼慢

    各缓存/内存访问周期参考下表
存储类型RegisterShared MemoryL1 CacheL2 CacheTexture/Const MemorySystem Memory
访问周期11~321~3232~64400~600400~600
5)GPU内存与缓存的数据交换


  • Cache line
      数据交换基本单位CPU的Cache line一般为64-bitGPU的Cache line一般为128-bit字节对齐
    • 可提前计算出是否“缓存命中”
        标记位 + 地址偏移(由Cache line决定) + 数据偏移  <-> 与现有数据比对


  • Memory Bank
      用于提高内存访问性能SMEM/L1 Cache被设计为一个个的 Memory Bank
    • Bank数量一般与Warp大小或者CudaCore数量对应
        比如一个SM有32个CudaCore,就把SMEM划分为32个Bank每个Bank包含多个Cache line每个Bank可以被独立访问(类似于食堂打饭的窗口)提升并行性能

    • Bank中的一条Cache line被若干线程同时访问
        数据可以通过广播机制同步到其他线程

    • Bank中的多条Cache line若被同时访问
        需要阻塞等待,串行访问这种情况叫:“Bank Conflict”影响性能


  • 缓存命中

    • 对性能影响巨大
        理由是对一块内存进行顺序访问比随机访问性能提高很多

    • 纹理的Mipmap可提高缓存命中率
        尽可能避免了采样远距离纹理时被迫从系统内存忘片上缓存拷贝的过程
      Unity ECS系统 -> 针对Cache友好的数据布局来提升性能

6)GPU运算系统总览

    SIMD(Single Instruction Multiple Data) 和 SIMT(Single Instruction Multiple Thread)
  • 单指令,多数据
      若干相同运算(指令)的输入(数据),会被打包成组,并行执行

  • 早期GPU将数据打包成Vector4来执行
      针对颜色的rgba四个分量对应SIMD -> Data就是大量的Vector4对应Vector processor(向量处理器)每个线程调用向量处理器(Vector ALU)操作向量寄存器完成运算面向数据(Data)的并行模式(DLP,Data Level Parallelism)不使用目前越来越复杂的计算需求

  • 现代GPU的改进
      避免浪费,更加通用对应SIMT -> Data是大量的Scalar对应Scalar processor(标量处理器)
    • 每个标量处理单元(Scalar ALU)对应一个像素线程
        所有ALU共享控制单元(如取指令/转码指令等模块)每个线程可以有自己的寄存器,独立的内存寻址通道,分支执行
      面向线程的并行模式(TLP,  Thread Level Parallelism)
    • 更进一步的是“超标量”(Super Scalar)
        面向指令级并行(ILP,Instruction Level Parallelism)


  • Mali - Midgard
      VLIM(Very Long Instruction Word)既超长指令字可以视为一个 Super Scalar ALU
    • 拥有128-bit 位宽的并行计算能力
        同时计算4个FP32或者8个FP16 (在一个线程内)
      编译器和GPU会合并指令,以便充分利用ALU的处理能力(资源)可以认为是一种指令级别的并行模式(ILP,Instruction Level Parallelism)

  • 目前PowerVR,Adreno 以及 Mali-Valhall架构的GPU都支持Super Scalar
      支持同时发射多个指令空闲的ALU负责执行

  • 归根到底还是SIMD
      GPU的计算单元本质都是在并行处理大量数据
    Vector processor V.S. Scalar processor 参考来源

SIMD vs SIMT


  • 向量处理器和标量处理器比对

    • 目标是对4套颜色的rgb分别执行一次乘法
        Four Multiplication against Four ColorsT0.xyz stands for the first colorT0 ~T3 is a group of colors we wanna handle

    • Vector processor在一个计算周期cycle中同时计算颜色的x,y和z分量(共3个值)
        如果没有填充蛮4个向量通道,则会如左图产生浪费计算4组颜色共需要4个cycle

    • Scalar processor允许在一个cycle中独立得处理4套数据
        灵活规划和分配计算通道(计算资源)在3个cycle内完成4组颜色的乘法需求


  • 在shader中合并若干个标量计算为向量计算

    • 在Scalar processor上并没有优化效果
        因为处理器在编译和运行时还是会把向量拆散


7)GPU中的线程束(Warp)


  • 是逻辑上的概念
      与之对应的物理概念为SM

  • 适应SIMT架构
      从属于一个Warp中的所有Thread同时执行相同的指令,只有附着在线程上的数据不同只需要一套控制单元去获取指令 + 解码指令可以减少每个Core的晶体管数目(不损失太多性能的前提下),从而减少功耗

  • Warp Scheduler
      周期性挑选(既切换到)处于激活状态的Warp将数据存入寄存器(Register File,D-Cache)将着色器代码存入指令缓存(I-Cache)
    • 驱动指令分派单元(DispatchUnit)
        读取指令缓存分派给计算核心(ALU)执行

    分支逻辑

Warp Divergent


  • Warp Divergence
      发生Warp中32个线程遇到if-else分支,部分需要走if,另一部分走else时锁步(Lock Step)执行要求同一时间处理一样的指令
    • GPU的处理方式:两个分支都走一遍
        当前线程的激活分支当前线程的非激活分支


  • 线程遮蔽(Masked Out)
      当前线程在运行到非激活分支时,仍然会执行指令逻辑(因为大家同步执行)通过线程遮蔽,丢弃中间执行结果

  • Shader中的分支逻辑困境
      除非全部线程走了一条分支,不然Warp相当于执行了一遍分支逻辑的所用可能不能按照CPU中对分支逻辑的惯性思维来估算GPU执行效率

  • 独立指令逻辑的最小单位
      Warp0.1份Warp的工作量 == 1份Warp的工作量
    只要指令是同一份,即便像素线程工作的图元不是同一个,也可能被放在同一个Warp下执行
  • Warp中线程数量
      不要求在物理上与SM中的Cuda Core保持一致(此处以Navida举例而已)
    • 假设物理上只有16个Core,一个Warp逻辑上仍然可以定义32个Thread
        此时赋予2倍周期的执行时间完成32个物理Core的活就行PowerVR具有类似设计


8)GPU的延迟影藏(Latency Hiding)


latency_hiden


  • 指令从开始到结束所消耗的 clock cycle 称为指令延迟(Latency)

    • 高延迟主要是由于访问了主存
        采样纹理,读取顶点数据,读取Varying时出现Cache missing所致约需要数百个时钟周期(400~600 cycles)


  • 回顾CPU的延迟影藏方案
      分支预测(90%以上准确率)乱序执行(Out-of-Order)合理规划执行顺序以压缩总耗时大容量、多级别的缓存(L1,L2,L3 Cache)
    • 超线程(Super Scalar)在2组寄存器之间较快切换线程
        本质也是切换上下文,但是开销仍然比GPU大


  • GPU的延迟影藏方案

    • 高效的上下文切换
        当一个Warp_A Stall了(干活干到长延迟指令了)时,可以立刻切换到下一个Warp_B当需要的数据准备完成,在合适的时机可以迅速切换回Warp_A继续执行后续指令


  • GPU延迟影藏的功臣 - 超大规模寄存器

    • 作为Warp(线程束)在物理端的宿主SM(Streaming Multiprocessor)
        拥有超过32,640个寄存器被物理上的32个Cuda Core分享

    • 每一个运行在Warp中的Thread
        最大可以使用255个寄存器

    • 因此,即便一个Warp中所有Thread都占满了各自的寄存器,也只用了:
        32 * 255 = 816032,640 * (1/4) = 8160既只用了全部物理寄存器的1/4


  • GPU延迟影藏的执行逻辑

    • 每一个SM会被同时分配多个Warp执行
        Warp是逻辑上的概念,SM是物理上的实体

    • Warp一旦与某个SM绑定,就不会再离开
        避免像CPU那样臃肿的上下文切换
      Warp中的Thread一开始就会被指派好所需的寄存器资源以及Local Memory
    • Warp在触发Stall时
        SM中的Core会立即切换到其他附着于当前SM的Warp期间无需保存/恢复寄存器状态总耗时 -> 1 * cycle

    • SM中的 Warp Scheduler(线程束调度器)会周期性的挑选 Active Warp 送去执行
        Selected Warp:被选中,即将或正在执行的Warp
      • Eligible Warp:准备就绪的Warp
          Selected Warp来自于这些Warp之中
        • 需要满足条件:
            32个Cuda Core处于空闲状态所有指令参数准备就绪


      • Stalled Warp:没有准备好的Warp
          可能是参数没准备好也可能是当前没有可用的Cuda Core



  • GPU延迟遮蔽逻辑背后的一些推论

    • Shader中变量如果很多(Shader写得长,整理的不好)
        会占用较多的寄存器数量变相减少了驻扎在当前SM中Warp的总数也就降低了找到Eligible Warp的可能性从而削弱了延迟遮蔽能力


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-15 19:25 , Processed in 0.094977 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表