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