<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 19 章 图形 on Go 语言原本</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/</link><description>Recent content in 第 19 章 图形 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/index.xml" rel="self" type="application/rss+xml"/><item><title>19.1 渲染管线与 Go 的位置</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/pipeline/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/pipeline/</guid><description>&lt;h1 id="191-渲染管线与-go-的位置"&gt;19.1 渲染管线与 Go 的位置&lt;/h1&gt;
&lt;p&gt;第 18 章把 GPU 当作一台「通用并行计算设备」来读。但 GPU 的本名是 &lt;strong&gt;Graphics&lt;/strong&gt; Processing Unit,
它最初、也最本职的工作是渲染图形。早在「用 GPU 做通用计算」成为口号之前的二十年，显卡就已经在
为屏幕上的每一个像素做并行运算了。所以图形是&lt;strong&gt;最古老的异构负载&lt;/strong&gt;,GPU 上那套大规模并行的硬件，
本就是为它而生。这一章回到这条本源，看 Go 在图形里扮演什么角色，而起点是看清那条贯穿一切的
&lt;strong&gt;渲染管线&lt;/strong&gt;,以及 Go 的代码究竟坐在它的哪个位置。&lt;/p&gt;
&lt;h2 id="1911-管线一条分段的数据流"&gt;19.1.1 管线：一条分段的数据流&lt;/h2&gt;
&lt;p&gt;把一堆三维顶点变成屏幕上一帧彩色图像，GPU 走的是一条&lt;strong&gt;分段的流水线&lt;/strong&gt;。每一段读入上一段的输出，
做一类固定的变换，再交给下一段。经典的图形管线大致是这样：&lt;/p&gt;

&lt;script src="https://golang.design/under-the-hood/mermaid.min.js"&gt;&lt;/script&gt;
&lt;script src="https://golang.design/under-the-hood/mermaid-init.js"&gt;&lt;/script&gt;


&lt;pre class="mermaid"&gt;flowchart LR
 app[&amp;#34;应用阶段&amp;lt;br/&amp;gt;(CPU / Go)&amp;#34;] --&amp;gt; vs[&amp;#34;顶点处理&amp;lt;br/&amp;gt;(可编程 shader)&amp;#34;]
 vs --&amp;gt; pa[&amp;#34;图元装配&amp;lt;br/&amp;gt;(固定)&amp;#34;]
 pa --&amp;gt; rs[&amp;#34;光栅化&amp;lt;br/&amp;gt;(固定)&amp;#34;]
 rs --&amp;gt; fs[&amp;#34;片元处理&amp;lt;br/&amp;gt;(可编程 shader)&amp;#34;]
 fs --&amp;gt; fb[&amp;#34;帧缓冲&amp;lt;br/&amp;gt;(固定)&amp;#34;]&lt;/pre&gt;
&lt;p&gt;这条管线里，有些段是&lt;strong&gt;固定功能&lt;/strong&gt;的（图元装配、光栅化、帧缓冲混合），由硬件写死，你只能配置参数；
有些段是&lt;strong&gt;可编程&lt;/strong&gt;的，顶点处理与片元处理各跑一段叫 &lt;strong&gt;shader&lt;/strong&gt;(着色器）的小程序，由你提供。
光栅化是这条线的心脏：它把一个三角形「填」成一片覆盖到的像素，决定了哪些片元需要被着色。
整条管线天然适合 GPU,因为每个顶点、每个片元都可以被同一段 shader 独立地、并行地处理,
这正是第 18 章说的 SIMT。&lt;/p&gt;
&lt;h2 id="1912-go-坐在哪里cpu-侧的编排者"&gt;19.1.2 Go 坐在哪里：CPU 侧的编排者&lt;/h2&gt;
&lt;p&gt;关键的问题来了：这条管线上，Go 的代码坐在哪一段？&lt;/p&gt;
&lt;p&gt;答案是&lt;strong&gt;最左边那一段，且仅此一段&lt;/strong&gt;:应用阶段。Go 跑在 CPU 上，它做的事是「准备数据、下达命令」:
把场景的顶点、纹理、变换矩阵组织好，上传到显存，然后发起一次次&lt;strong&gt;绘制调用&lt;/strong&gt;(draw call),
告诉 GPU「用这套数据、这套 shader，画」。一旦绘制调用发出，后面那几段顶点处理、光栅化、片元处理
全在 GPU 上跑，Go 不再介入，只在最后需要时把结果取回或交给窗口系统显示。&lt;/p&gt;</description></item><item><title>19.2 图形绑定与线程亲和</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/bindings/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/bindings/</guid><description>&lt;h1 id="192-图形绑定与线程亲和"&gt;19.2 图形绑定与线程亲和&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././pipeline"&gt;19.1&lt;/a&gt; 说 Go 坐在管线的应用阶段，负责发起绘制调用。可在真正发出第一条绘制调用
之前，有一道坎横在所有 Go 图形程序面前，它不来自图形本身，而来自 Go 的并发模型与图形 API 的
一个根本矛盾:&lt;strong&gt;图形上下文绑定线程，而 goroutine 会迁移线程&lt;/strong&gt;。这道坎是本节的主角，
而 &lt;a href="../../ch18gpu/sched"&gt;18.2.5&lt;/a&gt; 那把钥匙 &lt;code&gt;LockOSThread&lt;/code&gt;,在这里从一个可选的技巧变成了必需。&lt;/p&gt;
&lt;h2 id="1921-上下文一个绑定线程的隐式状态机"&gt;19.2.1 上下文：一个绑定线程的隐式状态机&lt;/h2&gt;
&lt;p&gt;OpenGL 这类图形 API 是围绕&lt;strong&gt;上下文&lt;/strong&gt;(context)组织的。上下文是一个庞大的隐式状态机:当前绑定
的着色器、纹理、缓冲、混合模式、视口……几乎所有 API 调用都不显式地接收上下文参数，而是隐式地
作用在「当前上下文」上。&lt;code&gt;glBindTexture&lt;/code&gt; 绑的是当前上下文里的纹理槽，&lt;code&gt;glDrawElements&lt;/code&gt; 用的是当前
上下文里的一整套状态。&lt;/p&gt;
&lt;p&gt;关键在于「当前」二字是&lt;strong&gt;按线程&lt;/strong&gt;定义的:一个 OpenGL 上下文在某个时刻「当前于」某一条特定的 OS
线程。你在线程 A 上把上下文设为当前、配置好状态、发出绘制调用，这一切都依附在线程 A 上。
如果同一串 OpenGL 调用里，有一部分跑到了线程 B 上,而线程 B 上并没有这个当前上下文,那些调用
要么直接失败，要么作用在一个空的上下文上，画面一片漆黑。&lt;/p&gt;
&lt;h2 id="1922-goroutine-会迁移于是必须钉住"&gt;19.2.2 goroutine 会迁移，于是必须钉住&lt;/h2&gt;
&lt;p&gt;这正是 Go 的并发模型撞上图形 API 的地方。回忆第 9 章:goroutine 不绑定固定的线程，调度器会
把它在不同的 M 之间&lt;strong&gt;迁移&lt;/strong&gt;,这次在线程 A 上跑，一次抢占、一次系统调用、一次通道阻塞之后，
下次很可能就被调度到线程 B 上继续。对纯 Go 代码，这种迁移是透明的、无害的，正是 M:N 调度的
红利。可对 OpenGL,它是灾难:&lt;/p&gt;</description></item><item><title>19.3 软件渲染与并行</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/software/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/software/</guid><description>&lt;h1 id="193-软件渲染与并行"&gt;19.3 软件渲染与并行&lt;/h1&gt;
&lt;p&gt;前两节的渲染都要过一道边界:把数据和命令交给 GPU,付清第 18 章那一整套过桥费，还要伺候图形上下文
的线程纪律（&lt;a href=".././bindings"&gt;19.2&lt;/a&gt;）。这一节走另一条路:&lt;strong&gt;软件渲染&lt;/strong&gt;,完全在 CPU 上算出每一个像素，
不碰 GPU、不碰驱动、不碰任何 FFI 边界。这条路一度被认为是「慢而无用的退路」,可它恰恰是把 Go
的并发能力，以及 Go 1.27 的 &lt;code&gt;simd&lt;/code&gt;,用在图形上的最佳舞台。&lt;/p&gt;
&lt;h2 id="1931-为什么还要软件渲染"&gt;19.3.1 为什么还要软件渲染&lt;/h2&gt;
&lt;p&gt;GPU 这么快，为什么还有人在 CPU 上渲染?因为有几类场景，GPU 要么用不上，要么不划算。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没有 GPU 可用。&lt;/strong&gt; 服务器端批量生成图片、缩略图、图表、PDF 渲染，跑在没有显卡、也没有显示器
的无头（headless）机器上。这是 Go 最主流的部署形态，恰恰也是 GPU 最缺席的地方。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要确定性与可移植。&lt;/strong&gt; 软件渲染的结果逐位可复现，不受驱动版本、显卡型号的影响。需要「同一份
输入在任何机器上渲染出逐像素相同的图」时(测试基线、文档生成），软件渲染是唯一可靠的选择。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图省心、要全控。&lt;/strong&gt; 没有上下文、没有线程纪律、没有边界，渲染器就是一段普通的 Go 代码，
可读、可调试、可单步,每一个像素怎么来的都看得见。Go 标准库的 &lt;code&gt;image&lt;/code&gt;、&lt;code&gt;image/draw&lt;/code&gt;、
&lt;code&gt;golang.org/x/image&lt;/code&gt;,以及社区里的纯 Go 渲染器（如 &lt;code&gt;polyred&lt;/code&gt;),走的都是这条路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这几条收成一句:软件渲染是&lt;strong&gt;边界的另一面&lt;/strong&gt;。第 18、19 章前面反复计算的过桥成本，在这里一笔
都不存在，代价是放弃了 GPU 的海量吞吐。于是问题从「怎么过桥更便宜」变成了「&lt;strong&gt;不过桥，怎么把
CPU 的并行榨干&lt;/strong&gt;」。答案有两层，正好对应第 18.4 节那个三种并行的分类里属于 CPU 的两种:
goroutine 的任务并行，与 SIMD 的数据并行。&lt;/p&gt;
&lt;h2 id="1932-把屏幕切成瓦片goroutine-级并行"&gt;19.3.2 把屏幕切成瓦片：goroutine 级并行&lt;/h2&gt;
&lt;p&gt;软件渲染有一个先天的好性质:&lt;strong&gt;像素之间大多互不依赖&lt;/strong&gt;。一帧图像上不同区域的像素，可以完全独立地
算出来。这是教科书级的「易并行」(embarrassingly parallel)问题,而 Go 的 goroutine 正是为这种
任务级并行生的。&lt;/p&gt;</description></item><item><title>19.4 浏览器中的渲染</title><link>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/wasm/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://golang.design/under-the-hood/zh-cn/part6hetero/ch19graphics/wasm/</guid><description>&lt;h1 id="194-浏览器中的渲染"&gt;19.4 浏览器中的渲染&lt;/h1&gt;
&lt;p&gt;前三节的渲染，要么把活儿推过 FFI 边界交给本机 GPU(19.1、19.2),要么留在 CPU 上软件渲染
（&lt;a href=".././software"&gt;19.3&lt;/a&gt;）。这一节把场景换到一个特别的运行环境:&lt;strong&gt;浏览器&lt;/strong&gt;。Go 可以编译成
WebAssembly(WASM)在浏览器里跑，而一旦进了浏览器，渲染会遇到一道全新的边界。有意思的是，
这道边界的形状、它的成本、应对它的办法，与前面整整两章讲的异构计算几乎一一对应,只是搬高了一层。
看懂这一节，就会发现「FFI 边界」是个比 cgo 宽得多的母题。&lt;/p&gt;
&lt;h2 id="1941-go-进入浏览器wasm-与-syscalljs"&gt;19.4.1 Go 进入浏览器：WASM 与 syscall/js&lt;/h2&gt;
&lt;p&gt;Go 用 &lt;code&gt;GOOS=js GOARCH=wasm&lt;/code&gt; 就能把程序编译成一个 &lt;code&gt;.wasm&lt;/code&gt; 模块，加载进网页，在浏览器的
WebAssembly 虚拟机里执行。但 WASM 模块本身是个&lt;strong&gt;沙盒&lt;/strong&gt;:它能做纯计算，却&lt;strong&gt;碰不到外面的世界&lt;/strong&gt;,
没有 DOM，没有画布，没有 GPU,这些都属于浏览器的 JavaScript 环境。&lt;/p&gt;
&lt;p&gt;WASM 与 JS 之间隔着一道膜，跨越它的桥是标准库的 &lt;code&gt;syscall/js&lt;/code&gt;。Go 代码通过 &lt;code&gt;js.Global()&lt;/code&gt; 拿到
JS 的全局对象，用 &lt;code&gt;js.Value&lt;/code&gt; 的 &lt;code&gt;Get&lt;/code&gt;/&lt;code&gt;Set&lt;/code&gt;/&lt;code&gt;Call&lt;/code&gt; 去读写 JS 属性、调用 JS 函数:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;span class="lnt"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;syscall/js&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Global&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;document&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;getElementById&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;screen&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;getContext&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;2d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 每一次 Get / Call，都是一次从 WASM 跨进 JS 的边界穿越&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;请认出这道膜的真面目:它就是又一条 &lt;strong&gt;FFI 边界&lt;/strong&gt;。&lt;code&gt;syscall/js&lt;/code&gt; 之于 WASM/JS,正如 cgo 之于
Go/C。每一次 &lt;code&gt;js.Value&lt;/code&gt; 的调用，都要把参数从 WASM 的线性内存里编组、跨过膜、进入 JS,
和 &lt;a href="../../ch18gpu/boundary"&gt;18.1&lt;/a&gt; 描述的跨界是同构的。于是 18.1 那条核心告诫原封不动地适用:
&lt;strong&gt;这道边界穿越有固定成本，要尽量少跨。&lt;/strong&gt;&lt;/p&gt;</description></item></channel></rss>