<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>星外之神 Blog</title>
    <description>天下难事，必作于易；天下大事，必作于细</description>
    <link>http://wszqkzqk.github.io/</link>
    <atom:link href="http://wszqkzqk.github.io/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Thu, 30 Apr 2026 01:53:28 +0000</pubDate>
    <lastBuildDate>Thu, 30 Apr 2026 01:53:28 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>LinSYS2：无需虚拟机与容器，在Linux上完整管理、构建、调试与运行Windows程序</title>
        <description>&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;

&lt;p&gt;在Linux上为Windows平台开发程序，现有方案各有各的痛点：交叉编译装的是Linux移植版工具链，构建行为与Windows原生环境未必一致，编译出的二进制既无法运行也无法调试；虚拟机资源占用大、启动慢、与宿主交互割裂；容器方案则由于Wine上游不支持非原生的依赖&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;msys-2.0.dll&lt;/code&gt;的程序，需要非标准的Wine fork，长期维护性存疑。&lt;/p&gt;

&lt;p&gt;有没有一种方案，&lt;strong&gt;像交叉编译一样轻量、像虚拟机一样完整，同时只依赖上游普通Wine就能工作&lt;/strong&gt;？&lt;/p&gt;

&lt;p&gt;笔者开发的&lt;a href=&quot;https://github.com/wszqkzqk/LinSYS2&quot;&gt;&lt;strong&gt;LinSYS2&lt;/strong&gt;&lt;/a&gt;正是为此而生。它直接在Linux上安装来自MSYS2官方仓库的&lt;strong&gt;Windows原生工具链和库&lt;/strong&gt;，通过Wine运行。两条命令，你就能在Linux shell里用上Windows版的GCC、GDB、CMake等程序——编译、调试、运行，全部在Linux下完成。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;No VM. No dual-boot. No containers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;linsys2是什么&quot;&gt;&lt;a href=&quot;https://github.com/wszqkzqk/LinSYS2&quot;&gt;LinSYS2&lt;/a&gt;是什么&lt;/h2&gt;

&lt;p&gt;LinSYS2是能在Linux上&lt;strong&gt;自动安装、管理Windows工具链和依赖库&lt;/strong&gt;的命令行工具。它在Linux端&lt;strong&gt;原生运行&lt;/strong&gt;一个&lt;strong&gt;适配后的MSYS2 pacman fork&lt;/strong&gt;，接入MSYS2官方仓库，将&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mingw-w64&lt;/code&gt;系列的Windows原生二进制包安装到用户目录下，随后你便可以像在Linux里调用普通命令一样，借助Wine运行这些Windows程序。&lt;/p&gt;

&lt;p&gt;LinSYS2的核心是在Linux上使用&lt;strong&gt;Linux原生的pacman fork&lt;/strong&gt;管理&lt;strong&gt;整个MSYS2的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mingw-w64&lt;/code&gt;官方包生态&lt;/strong&gt;，安装&lt;strong&gt;真正的Windows工具链&lt;/strong&gt;后通过&lt;strong&gt;Wine运行Windows程序&lt;/strong&gt;。它不提供模拟层，不维护独立的包集合，不fork上游仓库——它只是把MSYS2已经做好的工作，搬到了Linux桌面上。&lt;/p&gt;

&lt;p&gt;这意味着：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;你安装的工具链都是&lt;strong&gt;Windows原生版本&lt;/strong&gt;，构建行为与Windows完全一致&lt;/li&gt;
  &lt;li&gt;你链接的库与Windows上通过MSYS2安装的库&lt;strong&gt;完全相同&lt;/strong&gt;，版本、补丁、编译选项一致&lt;/li&gt;
  &lt;li&gt;通过Wine运行程序，启动速度接近原生，资源占用极低&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;核心特性一览&quot;&gt;核心特性一览&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;特性&lt;/th&gt;
      &lt;th&gt;说明&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;真正的Windows工具链&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;安装的是MSYS2官方仓库中的Windows版GCC/LLVM、GDB/LLDB、CMake/Meson等&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;完整的开发生命周期&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;安装包、编译代码、调试程序、运行测试、发布二进制，全部在Linux shell中完成&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;无需虚拟机与容器&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;通过Wine运行，启动速度接近原生，资源占用极低&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;用户级完全隔离&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;所有数据存储在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.local/share/linsys2/&lt;/code&gt;，无需root，不污染系统，&lt;strong&gt;默认不污染&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.wine&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;多架构目标支持&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;单台机器可同时维护&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ucrt64&lt;/code&gt;（GCC/x86_64）、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clang64&lt;/code&gt;（Clang/x86_64）、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clangarm64&lt;/code&gt;（Clang/ARM64）环境&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;原生包管理体验&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;直接使用MSYS2 pacman，数万mingw-w64包即装即用，自动处理依赖&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;安装&quot;&gt;安装&lt;/h2&gt;

&lt;h3 id=&quot;arch-linux&quot;&gt;Arch Linux&lt;/h3&gt;

&lt;p&gt;LinSYS2已发布到AUR，Arch Linux用户可以直接安装：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 使用yay&lt;/span&gt;
yay &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; linsys2

&lt;span class=&quot;c&quot;&gt;# 或使用paru&lt;/span&gt;
paru &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; linsys2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;AUR包通过GitHub Actions自动构建并发布，始终与主分支同步更新。&lt;/p&gt;

&lt;h3 id=&quot;其他发行版&quot;&gt;其他发行版&lt;/h3&gt;

&lt;p&gt;其他Linux发行版用户需要先安装对应的依赖包。各发行版的包名略有不同，以Debian/Ubuntu和Fedora为例：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debian/Ubuntu：&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;meson ninja-build gcc make git patch pkg-config &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    libarchive-dev libssl-dev libgpgme-dev libcurl4-openssl-dev &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    gawk gettext which gnupg wine python3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Fedora：&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;dnf &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;meson ninja-build gcc make git patch pkg-config &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    libarchive-devel openssl-devel gpgme-devel libcurl-devel &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    gawk gettext which gnupg wine python3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中，构建依赖为：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;meson&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ninja-build&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcc&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pkg-config&lt;/code&gt;以及&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libarchive&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libssl&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libgpgme&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcurl&lt;/code&gt;的开发包；运行时依赖为：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coreutils&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gawk&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gettext&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;which&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gnupg&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;openssl&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libarchive&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bzip2&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xz&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zstd&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wine&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python&lt;/code&gt;。不需要任何特殊补丁或定制版本的Wine，发行版自带的普通Wine即可正常工作。&lt;/p&gt;

&lt;p&gt;安装依赖后，通过源码编译安装：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone &lt;span class=&quot;nt&quot;&gt;--recursive&lt;/span&gt; https://github.com/wszqkzqk/LinSYS2.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;LinSYS2
make &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;make &lt;span class=&quot;nv&quot;&gt;PREFIX&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/usr &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;快速上手两条命令开始windows开发&quot;&gt;快速上手：两条命令开始Windows开发&lt;/h2&gt;

&lt;p&gt;LinSYS2的安装和使用都极为简洁：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 安装Windows版GCC&lt;/span&gt;
linsys2-pacman &lt;span class=&quot;nt&quot;&gt;-Sy&lt;/span&gt; mingw-w64-ucrt-x86_64-gcc

&lt;span class=&quot;c&quot;&gt;# 运行它&lt;/span&gt;
linsys2 run &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; gcc &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;只需两条命令，你就能在Linux上编译并运行Windows程序。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2-pacman&lt;/code&gt;会自动初始化环境、同步MSYS2官方数据库、安装包；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2 run&lt;/code&gt;会在隔离的Wine前缀中运行Windows GCC，输出与你直接在Windows MSYS2终端中运行&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcc -v&lt;/code&gt;完全一致的信息。&lt;/p&gt;

&lt;h3 id=&quot;典型工作流&quot;&gt;典型工作流&lt;/h3&gt;

&lt;h4 id=&quot;初始化环境与同步数据库&quot;&gt;初始化环境与同步数据库&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;linsys2-pacman &lt;span class=&quot;nt&quot;&gt;-Syu&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这与在Arch Linux或MSYS2中使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pacman -Syu&lt;/code&gt;的体验完全一致。LinSYS2会自动创建隔离的GPG keyring、配置文件和数据目录。&lt;/p&gt;

&lt;h4 id=&quot;安装开发工具链&quot;&gt;安装开发工具链&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# GCC工具链&lt;/span&gt;
linsys2-pacman &lt;span class=&quot;nt&quot;&gt;-Sy&lt;/span&gt; mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gdb

&lt;span class=&quot;c&quot;&gt;# CMake&lt;/span&gt;
linsys2-pacman &lt;span class=&quot;nt&quot;&gt;-Sy&lt;/span&gt; mingw-w64-ucrt-x86_64-cmake

&lt;span class=&quot;c&quot;&gt;# Python&lt;/span&gt;
linsys2-pacman &lt;span class=&quot;nt&quot;&gt;-Sy&lt;/span&gt; mingw-w64-ucrt-x86_64-python
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2-pacman&lt;/code&gt;完整支持pacman的命令行接口：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-S&lt;/code&gt;安装、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-R&lt;/code&gt;卸载、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Ss&lt;/code&gt;搜索、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Q&lt;/code&gt;查询已安装包、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Syu&lt;/code&gt;全系统升级。所有命令都与pacman原生行为一致，无需重新学习。&lt;/p&gt;

&lt;h4 id=&quot;编译与调试&quot;&gt;编译与调试&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 编译Windows可执行文件&lt;/span&gt;
linsys2 run &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; gcc &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; app.exe app.c

&lt;span class=&quot;c&quot;&gt;# 用Windows GDB调试&lt;/span&gt;
linsys2 run &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; gdb app.exe

&lt;span class=&quot;c&quot;&gt;# CMake构建&lt;/span&gt;
linsys2 run &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; cmake &lt;span class=&quot;nt&quot;&gt;-B&lt;/span&gt; build &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
linsys2 run &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; cmake &lt;span class=&quot;nt&quot;&gt;--build&lt;/span&gt; build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2 run&lt;/code&gt;使用&lt;strong&gt;隔离的Wine前缀&lt;/strong&gt;（位于&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.local/share/linsys2/{env}/wine&lt;/code&gt;），通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WINEPATH&lt;/code&gt;环境变量动态注入bin目录和DLL搜索路径。这意味着：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;无需手动配置Wine&lt;/strong&gt;，即开即用&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;不修改任何注册表&lt;/strong&gt;，环境完全无状态&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;不污染你现有的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.wine&lt;/code&gt;&lt;/strong&gt;，与日常运行其他Windows软件互不干扰&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;交互式shell&quot;&gt;交互式Shell&lt;/h4&gt;

&lt;p&gt;如果你需要频繁使用多个Windows工具，可以进入交互式子shell：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;linsys2 shell
&lt;span class=&quot;c&quot;&gt;# 现在所有Windows工具都在PATH中（但注意在Shell中运行时需要加上后缀名）&lt;/span&gt;
gcc.exe &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;
gdb.exe &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;
cmake.exe &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;
cmd.exe
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;多环境切换&quot;&gt;多环境切换&lt;/h4&gt;

&lt;p&gt;LinSYS2同时支持三种mingw-w64环境：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;环境名&lt;/th&gt;
      &lt;th&gt;编译器&lt;/th&gt;
      &lt;th&gt;默认目标架构&lt;/th&gt;
      &lt;th&gt;包前缀&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ucrt64&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;GCC (UCRT)&lt;/td&gt;
      &lt;td&gt;x86_64&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mingw-w64-ucrt-x86_64&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clang64&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Clang (UCRT)&lt;/td&gt;
      &lt;td&gt;—&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mingw-w64-clang-x86_64&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clangarm64&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Clang (UCRT)&lt;/td&gt;
      &lt;td&gt;ARM64&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mingw-w64-clang-aarch64&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;默认环境根据主机CPU架构自动检测（x86_64默认&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ucrt64&lt;/code&gt;，ARM64默认&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clangarm64&lt;/code&gt;），也可通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--env&lt;/code&gt;参数显式指定：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;linsys2-pacman &lt;span class=&quot;nt&quot;&gt;--env&lt;/span&gt; clang64 &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt; mingw-w64-clang-x86_64-llvm
linsys2 run &lt;span class=&quot;nt&quot;&gt;--env&lt;/span&gt; clang64 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; clang &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;与现有wine集成&quot;&gt;与现有Wine集成&lt;/h4&gt;

&lt;p&gt;如果你已经在Linux上使用Wine运行其他Windows软件，并且不愿意再创建额外的Wine环境，也可以选择将LinSYS2环境注册到现有Wine前缀中：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;linsys2 register    &lt;span class=&quot;c&quot;&gt;# 将LinSYS2的相关目录添加到Wine注册表PATH&lt;/span&gt;
linsys2 &lt;span class=&quot;nb&quot;&gt;env&lt;/span&gt;         &lt;span class=&quot;c&quot;&gt;# 查看注册状态&lt;/span&gt;
linsys2 unregister  &lt;span class=&quot;c&quot;&gt;# 从Wine PATH中移除&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;技术实现要点&quot;&gt;技术实现要点&lt;/h2&gt;

&lt;h3 id=&quot;为什么必须使用msys2-pacman-fork&quot;&gt;为什么必须使用MSYS2 pacman fork&lt;/h3&gt;

&lt;p&gt;MSYS2维护了一个pacman的fork，其中包含34个补丁。LinSYS2无法直接使用Arch Linux自带的pacman，原因只有一个：&lt;strong&gt;epoch分隔符&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在Arch Linux中，软件包的epoch（当版本号 scheme 变更时用于强制升级的前缀）使用冒号&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:&lt;/code&gt;分隔，例如&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1:2.0-1&lt;/code&gt;。但Windows文件名不能包含&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:&lt;/code&gt;，因此MSYS2将epoch分隔符改为波浪号&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&lt;/code&gt;，格式为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1~2.0-1&lt;/code&gt;。MSYS2的第13号补丁正是修改了这一行为：&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// lib/libalpm/version.c&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#if defined(__MSYS__) || defined(MSYS2_PACMAN_LINUX)
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;~&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// MSYS2 使用 ~ 分隔 epoch&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#else
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;:&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// Arch 使用 : 分隔 epoch&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;#endif
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;MSYS2官方仓库中存在大量使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&lt;/code&gt;作为epoch分隔符的包。如果Linux端使用原生pacman（期望&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:&lt;/code&gt;），这些包的版本号将无法被正确解析和比较，导致数据库同步失败、依赖解析错乱、安装命令无法匹配包名。&lt;strong&gt;因此，必须在Linux下构建MSYS2的pacman fork，这是不可回避的技术前提。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LinSYS2的解决方案非常精巧：通过一个统一的补丁（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches/0001-LinSYS2-Adapt-MSYS2-pacman-for-Linux.patch&lt;/code&gt;），将MSYS2 fork中所有关键的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#ifdef __MSYS__&lt;/code&gt;条件编译扩展为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#if defined(__MSYS__) || defined(MSYS2_PACMAN_LINUX)&lt;/code&gt;，同时在meson构建时通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-DMSYS2_PACMAN_LINUX&lt;/code&gt;宏启用这些代码路径。这样，MSYS2 fork中绝大多数适配逻辑在Linux构建时自动生效。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches/0001-LinSYS2-Adapt-MSYS2-pacman-for-Linux.patch&lt;/code&gt;处理了以下关键差异：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;epoch分隔符&lt;/strong&gt;：启用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&lt;/code&gt;分隔符，使版本解析与MSYS2仓库兼容&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;权限模型&lt;/strong&gt;：跳过root权限检查，支持普通用户运行（RootDir设在用户目录下）&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;文件提取权限&lt;/strong&gt;：非root用户跳过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ARCHIVE_EXTRACT_OWNER&lt;/code&gt;，避免权限错误&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;工具路径固定&lt;/strong&gt;：在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$MSYS2_PACMAN_LINUX&lt;/code&gt;环境下使用固定工具路径，避免与系统工具冲突&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CR处理&lt;/strong&gt;：从stdin读取时正确处理&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\r\n&lt;/code&gt;换行符&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;包格式与数据库的跨平台兼容性&quot;&gt;包格式与数据库的跨平台兼容性&lt;/h3&gt;

&lt;p&gt;LinSYS2能够直接复用MSYS2官方仓库，还得益于：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;数据库层100%兼容&lt;/strong&gt;：MSYS2与Arch Linux使用完全相同的ALPM数据库格式（版本9）。pacman的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;desc&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;files&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mtree&lt;/code&gt;文件结构、tar归档格式、PGP签名机制在Linux下可直接读写，无需任何转换。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;包格式层100%兼容&lt;/strong&gt;：mingw-w64包就是标准的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pkg.tar.zst&lt;/code&gt;（libarchive格式），pacman的提取过程是纯文件解压操作，完全不涉及任何Windows API，也不关心包内文件是ELF还是PE。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;依赖解析层100%兼容&lt;/strong&gt;：pacman的依赖解析是纯字符串匹配，只比较包名和版本号，从不检查包内文件的实际格式。这意味着mingw-w64包的依赖关系在Linux端可以被完美解析和处理。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;架构字段无冲突&lt;/strong&gt;：mingw-w64包的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arch&lt;/code&gt;字段通常为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;any&lt;/code&gt;，与Linux主机的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x86_64&lt;/code&gt;或&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aarch64&lt;/code&gt;不会产生冲突。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因此，LinSYS2能够在Linux上直接使用MSYS2的包管理系统，安装和管理Windows原生工具链和库，而几乎无需特殊处理。&lt;/p&gt;

&lt;h3 id=&quot;wine前缀管理与环境注入策略&quot;&gt;Wine前缀管理与环境注入策略&lt;/h3&gt;

&lt;p&gt;LinSYS2提供了两种Wine集成模式，以适应不同使用场景。&lt;/p&gt;

&lt;h4 id=&quot;独立wineprefix模式默认推荐&quot;&gt;独立WINEPREFIX模式（默认推荐）&lt;/h4&gt;

&lt;p&gt;独立WINEPREFIX模式下每个环境拥有独立的Wine前缀（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.local/share/linsys2/{env}/wine&lt;/code&gt;）。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2 run&lt;/code&gt;在运行程序前，会自动构造包含bin目录的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WINEPATH&lt;/code&gt;环境变量传递给Wine。DLL搜索路径通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WINEPATH&lt;/code&gt;注入，&lt;strong&gt;不修改任何注册表键值&lt;/strong&gt;。这种模式的优势在于：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;与用户的日常Wine环境（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.wine&lt;/code&gt;）彻底隔离，避免任何潜在的冲突或污染&lt;/li&gt;
  &lt;li&gt;多个LinSYS2环境之间互不干扰&lt;/li&gt;
  &lt;li&gt;删除环境时只需删除对应目录，无残留&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;现有wine环境集成&quot;&gt;现有Wine环境集成&lt;/h4&gt;

&lt;p&gt;使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2 register&lt;/code&gt;，将环境bin目录写入现有Wine前缀的注册表PATH中可以实现在现有Wine环境中集成LinSYS2。对于不希望额外创建单独Wine前缀的用户，这种模式提供了更直接的集成方式，可以将现有Wine环境下的内容直接和LinSYS2集成。&lt;/p&gt;

&lt;p&gt;无论哪种模式，LinSYS2都会自动处理Windows路径与Unix路径的转换，在从LinSYS2中运行时还会禁用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;winemenubuilder.exe&lt;/code&gt;以避免污染桌面菜单和MIME关联。&lt;/p&gt;

&lt;h3 id=&quot;用户级隔离与零系统冲突&quot;&gt;用户级隔离与零系统冲突&lt;/h3&gt;

&lt;p&gt;LinSYS2仅在用户目录下进行安装和管理：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;数据目录&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.local/share/linsys2/{env}/&lt;/code&gt;，存放实际安装的文件、包数据库和缓存&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;配置目录&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/linsys2/&lt;/code&gt;，存放各环境独立的pacman配置文件和镜像列表&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Wine前缀&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.local/share/linsys2/{env}/wine/&lt;/code&gt;，独立的Wine环境&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GPG keyring&lt;/strong&gt;：每个环境拥有独立的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;etc/pacman.d/gnupg/&lt;/code&gt;，互不干扰&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这种设计下，安装和运行LinSYS2&lt;strong&gt;完全不需要root权限&lt;/strong&gt;，也不会与系统的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pacman&lt;/code&gt;包管理器产生任何冲突。你可以在Arch Linux上同时使用系统pacman管理软件包，用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;linsys2-pacman&lt;/code&gt;管理Windows工具链，两者分别存储，互不干扰。&lt;/p&gt;

&lt;h3 id=&quot;install-scriptlet与hook的处理&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.install&lt;/code&gt; Scriptlet与Hook的处理&lt;/h3&gt;

&lt;p&gt;mingw-w64包极少包含&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.install&lt;/code&gt;安装脚本（scriptlet），绝大多数是纯文件包。LinSYS2的默认策略是使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--noscriptlet&lt;/code&gt;跳过scriptlet执行，这既避免了Windows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.exe&lt;/code&gt;安装脚本在Linux下无法直接运行的问题，也符合mingw-w64包的实际分布情况。&lt;/p&gt;

&lt;p&gt;对于极少数确实需要scriptlet的包，LinSYS2的架构也预留了通过Wine代理执行的扩展空间。Hook脚本的情况类似——mingw-w64包几乎不使用pacman hook，因此当前实现中无需特别处理。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;横向对比总结&quot;&gt;横向对比总结&lt;/h2&gt;

&lt;p&gt;为了更直观地展现LinSYS2的定位，笔者将几种常见方案做一个全面的横向对比：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;维度&lt;/th&gt;
      &lt;th&gt;传统交叉编译&lt;/th&gt;
      &lt;th&gt;虚拟机（VM）&lt;/th&gt;
      &lt;th&gt;容器&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;LinSYS2&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;构建工具链来源&lt;/td&gt;
      &lt;td&gt;Linux发行版移植&lt;/td&gt;
      &lt;td&gt;Windows原生&lt;/td&gt;
      &lt;td&gt;Windows原生&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;MSYS2官方仓库的Windows原生工具链&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;构建行为一致性&lt;/td&gt;
      &lt;td&gt;可能与Windows不同&lt;/td&gt;
      &lt;td&gt;与Windows相同&lt;/td&gt;
      &lt;td&gt;与Windows相同&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;与Windows相同&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;运行生成的二进制文件&lt;/td&gt;
      &lt;td&gt;不能&lt;/td&gt;
      &lt;td&gt;能&lt;/td&gt;
      &lt;td&gt;能&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;能（通过Wine）&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;使用Windows GDB调试&lt;/td&gt;
      &lt;td&gt;不能&lt;/td&gt;
      &lt;td&gt;能&lt;/td&gt;
      &lt;td&gt;能&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;能&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;库文件来源&lt;/td&gt;
      &lt;td&gt;发行版打包&lt;/td&gt;
      &lt;td&gt;Windows安装&lt;/td&gt;
      &lt;td&gt;镜像内安装&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;MSYS2官方仓库，与Windows一致&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;包管理器&lt;/td&gt;
      &lt;td&gt;无或发行版方案&lt;/td&gt;
      &lt;td&gt;Windows/手动&lt;/td&gt;
      &lt;td&gt;手动配置&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;MSYS2 pacman（原生体验）&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;依赖自动解析&lt;/td&gt;
      &lt;td&gt;有限&lt;/td&gt;
      &lt;td&gt;手动&lt;/td&gt;
      &lt;td&gt;手动&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;完整自动依赖解析&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;启动速度&lt;/td&gt;
      &lt;td&gt;快&lt;/td&gt;
      &lt;td&gt;慢（需启动完整OS）&lt;/td&gt;
      &lt;td&gt;中等（需启动容器）&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;快（Wine近原生速度）&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;资源占用&lt;/td&gt;
      &lt;td&gt;低&lt;/td&gt;
      &lt;td&gt;高（数GB内存+磁盘）&lt;/td&gt;
      &lt;td&gt;中等（镜像体积大）&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;低（与Wine相当）&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;用户权限要求&lt;/td&gt;
      &lt;td&gt;通常需要root&lt;/td&gt;
      &lt;td&gt;需虚拟化支持&lt;/td&gt;
      &lt;td&gt;通常需要root&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;用户级，无需root&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;系统集成度&lt;/td&gt;
      &lt;td&gt;与系统深度集成&lt;/td&gt;
      &lt;td&gt;完全隔离&lt;/td&gt;
      &lt;td&gt;隔离&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;可选隔离或渐进式集成&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;环境管理&lt;/td&gt;
      &lt;td&gt;手动&lt;/td&gt;
      &lt;td&gt;手动&lt;/td&gt;
      &lt;td&gt;手动&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;多环境并行，一键切换&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;从表格中可以清晰看出，LinSYS2在&lt;strong&gt;轻量性&lt;/strong&gt;上接近传统交叉编译，在&lt;strong&gt;功能完整性&lt;/strong&gt;上媲美虚拟机，同时提供了&lt;strong&gt;原生的包管理体验&lt;/strong&gt;——这在以往的所有方案中都是缺失的。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;LinSYS2的设计哲学非常简洁：&lt;strong&gt;不重复造轮子，把已经运转良好的生态桥接到新的平台&lt;/strong&gt;。它不fork MSYS2的包仓库，不维护独立的工具链，不做任何对包内容的修改——它只是利用ALPM数据库的跨平台兼容性、pacman依赖解析的格式无关性，Wine对Windows PE程序的执行能力，以及pacman对Linux的原生支持，将这四者巧妙地串联起来。&lt;/p&gt;

&lt;p&gt;对于需要在Linux上开发Windows应用程序的开发者而言，LinSYS2提供了一个&lt;strong&gt;轻量、高效、行为一致&lt;/strong&gt;的解决方案。你无需离开熟悉的Linux开发环境，无需忍受虚拟机的臃肿，就能获得与Windows上完全相同的工具链和构建流程。&lt;/p&gt;

&lt;p&gt;回顾笔者之前对Wine定位的思考，LinSYS2正是这种思路的延续：&lt;strong&gt;不再将Wine视为填补生态缺口的双刃剑，将Wine用作跨平台开发的基础设施&lt;/strong&gt;。当开源开发者能如此便捷地在Linux下为Windows构建、调试、测试原生应用时，使用Linux的开发者将得以进一步减少对Windows的依赖，真正实现跨平台开发的无缝体验。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;项目地址&lt;/strong&gt;：&lt;a href=&quot;https://github.com/wszqkzqk/LinSYS2&quot;&gt;https://github.com/wszqkzqk/LinSYS2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AUR地址&lt;/strong&gt;：&lt;a href=&quot;https://aur.archlinux.org/packages/linsys2/&quot;&gt;linsys2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;开源协议&lt;/strong&gt;：&lt;a href=&quot;https://www.gnu.org/licenses/old-licenses/gpl-2.0.html&quot;&gt;GPL v2 or later&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AI文档与问答&lt;/strong&gt;：&lt;a href=&quot;https://deepwiki.com/wszqkzqk/LinSYS2&quot;&gt;https://deepwiki.com/wszqkzqk/LinSYS2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 27 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/27/LinSYS2/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/27/LinSYS2/</guid>
        
        <category>开源软件</category>
        
        <category>Wine</category>
        
        <category>MSYS2</category>
        
        <category>跨平台开发</category>
        
        <category>archlinux</category>
        
        
      </item>
    
      <item>
        <title>Qt Web Extractor：适配 Anubis 并与 Qt HTML 解析斗智斗勇</title>
        <description>&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 最近遇到了两个比较复杂的问题：一个是 Anubis 反爬虫的 PoW 挑战页，另一个是 Qt &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt; 解析大页面 HTML 时的内存失控。排查下来发现，前者的修复其实非常简洁；后者虽然还没彻底解决，但恰好被前者的修复方式给自然缓解了。&lt;/p&gt;

&lt;h2 id=&quot;anubis其实适配很简单&quot;&gt;Anubis：其实适配很简单&lt;/h2&gt;

&lt;h3 id=&quot;现象&quot;&gt;现象&lt;/h3&gt;

&lt;p&gt;笔者在测试提取 &lt;a href=&quot;https://gitlab.winehq.org/wine/wine&quot;&gt;gitlab.winehq.org/wine/wine&lt;/a&gt; 时，发现返回的总是这段内容：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Making sure you’re not a bot!&lt;/p&gt;

  &lt;p&gt;Loading…&lt;/p&gt;

  &lt;p&gt;You are seeing this because the administrator of this website has set up Anubis to protect the server…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;拿不到后面的真实 GitLab 内容。&lt;/p&gt;

&lt;h3 id=&quot;分析&quot;&gt;分析&lt;/h3&gt;

&lt;p&gt;Anubis 是一种基于 PoW（Proof-of-Work）的反爬虫中间件。服务器向浏览器下发一个 SHA-256 计算挑战，客户端需要在本地通过 JavaScript brute-force 找出一个满足难度要求的 nonce，完成后通过 cookie 才能访问真实页面。&lt;/p&gt;

&lt;p&gt;跟 Cloudflare 那种复杂的指纹识别和行为分析不同，Anubis 的核心逻辑其实非常直白：&lt;strong&gt;它就是要求你付出算力&lt;/strong&gt;。对于真正的浏览器来说，这意味着在本地执行几秒的 JavaScript 计算。对于爬虫来说，这意味着大规模抓取的成本会急剧上升。&lt;/p&gt;

&lt;p&gt;而 Qt Web Extractor 本身就是一个完整的 Chromium headless 环境，有完整的 JavaScript 引擎，执行 PoW 计算完全不在话下。实际上笔者观察过，Anubis 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.mjs&lt;/code&gt; 在 Qt WebEngine 中可以正常执行，PoW 计算和后续重定向都没有问题。&lt;/p&gt;

&lt;p&gt;真正的问题在于&lt;strong&gt;提取器和 Anubis 的工作流没有对齐&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;Qt Web Extractor 的流程是：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.load(url)&lt;/code&gt; 加载页面&lt;/li&gt;
  &lt;li&gt;等 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt; 信号&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt; 后启动一个 2 秒的单发定时器&lt;/li&gt;
  &lt;li&gt;定时器触发后，注入 JavaScript 递归遍历 DOM，序列化成 HTML&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_finish()&lt;/code&gt; 设置 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settled = True&lt;/code&gt;，提取结束&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Anubis 挑战页的初始 HTML 很小（约 4KB），&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt; 在几百毫秒内就会触发。但此时页面上的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.mjs&lt;/code&gt; 才刚刚开始执行 PoW 计算，整个计算过程需要几秒到十几秒。计算完成后 JavaScript 调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.location.replace()&lt;/code&gt; 重定向到真实页面，这会触发新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt; 周期。&lt;/p&gt;

&lt;p&gt;但原来的代码只连接了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt;，没有连接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt;。所以 2 秒定时器触发后，提取器拿到当时的 DOM（挑战页），调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_finish()&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settled = True&lt;/code&gt;。这时候 Anubis 的 PoW 可能还在进行，甚至可能尚未启动。等几秒后 PoW 计算完成、页面重定向到真实内容、新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt; 触发——但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settled&lt;/code&gt; 已经是 True，所有后续处理都被跳过。真实页面就这样被忽略了。&lt;/p&gt;

&lt;p&gt;也就是说，PoW 能够正常计算完成，重定向也会正常执行，但提取器在重定向触发之前就已经 settled，后续的真实内容完全没有机会进入提取流程。&lt;/p&gt;

&lt;h3 id=&quot;修复&quot;&gt;修复&lt;/h3&gt;

&lt;p&gt;修复的思路是：既然提取器本身就能正常执行 PoW，那我们只需要&lt;strong&gt;等待计算完成&lt;/strong&gt;即可。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.location.replace()&lt;/code&gt; 会触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt;，这是感知自刷新的最直接方式。笔者在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_WebPage&lt;/code&gt; 中加了对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt; 的连接：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loadFinished&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_on_load_finished&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loadStarted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_on_load_started&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_on_load_started&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_stability_timer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Anubis 完成 PoW 触发重定向时，旧的 stability timer 会被停止。随后真实页面加载完成触发新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadFinished&lt;/code&gt;，重新启动 2 秒倒计时，在真实页面稳定后才提取。&lt;/p&gt;

&lt;p&gt;看 commit 记录就知道了——&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e993fec&lt;/code&gt; 这个 commit 只改了 5 行代码：加了一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_on_load_started&lt;/code&gt; 方法、连接了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt; 信号、修正了一个空白字符。Anubis 这种看似强力的反爬虫，对于一个本身就有完整 Chromium 内核的提取器来说，适配成本相当低。它不像 Cloudflare 那样需要绕过复杂的指纹识别，其核心只是要求客户端执行一段 JavaScript 计算。&lt;/p&gt;

&lt;p&gt;修复后的行为是&lt;strong&gt;概率性&lt;/strong&gt;的，取决于 PoW 计算时间和网络延迟的相对关系：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果 PoW 在 2 秒定时器触发前完成，重定向发生时 timer 会被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt; 重置，最终能成功提取真实页面。对于 winehq.org 这种难度较低的配置（PoW 约 2-3 秒），这种情况出现的概率很高，所以”大多数 Anubis 已经能够正常处理”。&lt;/li&gt;
  &lt;li&gt;如果 PoW 超过 2 秒，定时器先触发，提取器 settled，后续真实页面被忽略。kernel.org 的部分配置难度为 5，PoW 可能需要 5-8 秒，这种情况下会提取到挑战页。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;笔者也尝试过延长 stability delay 到 5 秒或更长，让 PoW 有更充分的时间完成。但这个方向很快就被放弃了，原因跟下面要说的内存问题直接相关。&lt;/p&gt;

&lt;h2 id=&quot;qt-html-解析的内存失控&quot;&gt;Qt HTML 解析的内存失控&lt;/h2&gt;

&lt;h3 id=&quot;现象-1&quot;&gt;现象&lt;/h3&gt;

&lt;p&gt;在测试提取 &lt;a href=&quot;https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=cdd4dc3aebeab43a72ce0bc2b5bab6f0a80b97a5&quot;&gt;Linux 内核的一个大型 merge commit&lt;/a&gt; 时，Python 进程在提取阶段被系统 OOM killer 杀掉。同一个页面在 Chrome 和 Firefox 中打开完全正常，内存占用也就几百 MB。但通过 Qt Web Extractor 提取时，内存占用一路飙升到 30GB 以上，直到耗尽。&lt;/p&gt;

&lt;h3 id=&quot;定位&quot;&gt;定位&lt;/h3&gt;

&lt;p&gt;问题出在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument.setHtml()&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;Qt Web Extractor 的提取管线中，HTML 到 Markdown 的转换依赖 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;QTextDocument&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setHtml&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;md&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;toMarkdown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个 commit 页面的 diff 内容非常大，cgit 用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;table&amp;gt;&lt;/code&gt; 展示 diff，每行一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;。浏览器渲染这种页面没有问题，因为浏览器的光栅化和布局是流式的。但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt; 是富文本处理类，内部会把 HTML 转换成块结构、表格结构和格式对象。对于包含数万行表格的代码 diff 页面，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setHtml()&lt;/code&gt; 内部需要为每个单元格分配格式对象和布局元数据，内存膨胀非常严重。&lt;/p&gt;

&lt;p&gt;笔者通过诊断脚本确认了规模：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;body.innerHTML.length = 2057483
outer_html_len = 2058590
inner_text_len = 1171958
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;2MB 的原始 HTML 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt; 中膨胀到 30GB 以上，这是 Qt 解析器的问题。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt; 的设计目标是处理编辑器量级的富文本，不是作为通用的任意大小 HTML 解析器。&lt;/p&gt;

&lt;h3 id=&quot;尝试与回退&quot;&gt;尝试与回退&lt;/h3&gt;

&lt;p&gt;笔者一度在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_text_from_html&lt;/code&gt; 中加入了大小阈值判断，对大页面降级为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;innerText&lt;/code&gt; 提取，绕过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt;。但这样做其实是在掩盖问题，而不是解决它。而且 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;innerText&lt;/code&gt; 丢失了 Markdown 的超链接和表格结构，输出质量明显下降。&lt;/p&gt;

&lt;p&gt;经过考虑，笔者回退了这个降级修改。2MB 的 HTML 不应该让任何解析器吃掉 30GB 内存，这是 Qt 的问题。在找到更根本的解决方案之前，不应该用降级来掩盖。&lt;/p&gt;

&lt;h3 id=&quot;与大页面内存问题的交叉影响&quot;&gt;与大页面内存问题的交叉影响&lt;/h3&gt;

&lt;p&gt;这里有一个需要说明的情况。kernel commit 这种大页面，真实内容本身就很大（约 2MB HTML），一旦进入 Qt 解析路径就会 OOM。笔者测试过，如果延长 stability delay 到 5 秒或更长，给 Anubis 更充分的时间完成重定向，真实页面被提取后内存会直接爆掉。&lt;/p&gt;

&lt;p&gt;保持 2 秒的 delay 反而避免了这个问题。这个巨大的页面在 2 秒内通常无法完成到真实页面的重定向。2 秒定时器触发后，提取器拿到 Anubis 挑战页（只有 4KB，安全），然后 settled。后续即使网络恢复、重定向到真实页面，由于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settled&lt;/code&gt; 已经是 True，真实大页面永远不会被提取——所以 Qt 解析大页面的 OOM 也永远不会触发。&lt;/p&gt;

&lt;p&gt;换句话说，大页面的真实内容因为 Anubis 未完成而被跳过，但这也恰好绕过了 Qt 解析器的内存失控。这不是特意设计的保护机制，而是当前网络环境和 2 秒 delay 共同产生的实际效果。&lt;/p&gt;

&lt;p&gt;目前的状态是小页面（如 gitlab.winehq.org/wine/wine）的 Anubis 流程顺畅，大概率能在 2 秒内完成重定向，能正常提取；大页面 Anubis 运算后在 2 秒内通常无法完成页面内容加载，提取到的是挑战页，真实内容拿不到，但至少不会 OOM。&lt;/p&gt;

&lt;h2 id=&quot;内容丢失问题&quot;&gt;内容丢失问题&lt;/h2&gt;

&lt;p&gt;在排查过程中，笔者还遇到了两个内容提取不完整的问题，修复方式各不相同。&lt;/p&gt;

&lt;h3 id=&quot;checkvisibility-的误判&quot;&gt;checkVisibility 的误判&lt;/h3&gt;

&lt;p&gt;之前的 Shadow DOM flatten JS 中加入了这行：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkVisibility&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;checkVisibility&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;意图是过滤掉不可见元素。但在某些现代网页中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkVisibility()&lt;/code&gt; 会因为计算时机、CSS 动画状态或元素尚未进入 viewport 而返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;，导致可见内容被错误过滤。笔者在测试 &lt;a href=&quot;https://newsletter.semianalysis.com/p/nvidia-tensor-core-evolution-from-volta-to-blackwell&quot;&gt;SemiAnalysis Newsletter&lt;/a&gt; 的一篇文章时，提取结果大量缺失——正文被全部过滤掉了。&lt;/p&gt;

&lt;p&gt;修复是直接移除了这行（commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;948ed7a&lt;/code&gt;）。不可见元素的过滤应该属于内容处理的后续阶段，不应该在 DOM 序列化时做硬拦截。&lt;/p&gt;

&lt;h3 id=&quot;dom-遍历范围的遗漏&quot;&gt;DOM 遍历范围的遗漏&lt;/h3&gt;

&lt;p&gt;另一个问题出在 DOM 序列化的入口点上。原来的代码从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.body&lt;/code&gt; 开始遍历：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;walk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这导致某些页面的 head 内容无法被正确捕获，提取结果异常。笔者在测试 &lt;a href=&quot;https://opensource.googleblog.com/2024/04/introducing-jpegli-new-jpeg-coding-library.html&quot;&gt;Google Open Source Blog&lt;/a&gt; 的一篇文章时，提取结果几乎只剩一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;——正文结构完全丢失。&lt;/p&gt;

&lt;p&gt;修复是将遍历入口改为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.documentElement&lt;/code&gt;，并扩展 SKIP 集合以显式排除 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;meta&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;link&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;base&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;title&lt;/code&gt; 等标签（commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8ca4503&lt;/code&gt;）：&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;SKIP&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;script&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;style&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;svg&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;noscript&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;base&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;walk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;documentElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;目前代码中已提交的修复包括：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadStarted&lt;/code&gt; 信号连接，在导航时重置 stability timer&lt;/li&gt;
  &lt;li&gt;移除了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkVisibility&lt;/code&gt; 过滤&lt;/li&gt;
  &lt;li&gt;DOM 遍历入口从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.body&lt;/code&gt; 改为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.documentElement&lt;/code&gt;，并扩展 SKIP 标签集合&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;stability delay 保持 2 秒不变。Anubis 这种基于 PoW 的反爬虫系统，对于一个本身就有完整 Chromium 内核的 headless 提取器来说，适配成本非常低——它不需要绕过任何复杂检测，只需要等待 PoW 计算完成即可。整个核心修复只有几行代码。&lt;/p&gt;

&lt;p&gt;Qt 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;QTextDocument&lt;/code&gt; 在处理大表格 HTML 时的内存失控，笔者已经确认是 Qt 侧的问题，需要进一步研究是 Qt 版本相关的已知问题，还是特定 HTML 结构触发的边缘情况。在找到可靠的修复或替代方案之前，这是项目的一个已知限制。而当前 2 秒的 delay 配置恰好避免了实际触发这个问题，算是一个意外的平衡。&lt;/p&gt;

&lt;p&gt;项目仓库地址：&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;GitHub · Qt Web Extractor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;项目协议：&lt;a href=&quot;https://www.gnu.org/licenses/gpl-3.0.html&quot;&gt;GPL-3.0-or-later&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/20/qt-web-extractor-anubis-and-parsing-pitfalls/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/20/qt-web-extractor-anubis-and-parsing-pitfalls/</guid>
        
        <category>Python</category>
        
        <category>Qt</category>
        
        <category>PySide</category>
        
        <category>开源软件</category>
        
        <category>网页提取</category>
        
        
      </item>
    
      <item>
        <title>PvZ-Portable：跨平台光标系统的 SDL 实现与自定义光标缓存</title>
        <description>&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;在将 PvZ-Portable 从 Windows 原生引擎改造为跨平台项目的过程中，渲染、音频、输入等核心子系统都经历了大规模重构。但有一个看似不起眼的模块长期被搁置——&lt;strong&gt;光标系统&lt;/strong&gt;。旧代码里，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp::EnforceCursor()&lt;/code&gt; 中躺着一大段被注释掉的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::SetCursor&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadCursor&lt;/code&gt; Win32 API 调用，事实上，游戏此前一直并没有实现跨平台光标处理。&lt;/p&gt;

&lt;p&gt;光标系统虽小，却是玩家与游戏交互的第一触点。当玩家悬停在可点击的按钮上时需要手型光标，在文本输入框中需要 I 形光标，在战斗场景中还需要隐藏光标以避免干扰沉浸感。本文将记录笔者如何&lt;strong&gt;彻底移除 Windows 专属的光标代码&lt;/strong&gt;，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SexyAppBase&lt;/code&gt; 中基于 SDL 实现一套完整的跨平台光标系统，并解决自定义光标创建与运行时缓存的技术细节。&lt;/p&gt;

&lt;h2 id=&quot;被遗留的-windows-光标实现&quot;&gt;被遗留的 Windows 光标实现&lt;/h2&gt;

&lt;p&gt;原版引擎的光标管理集中在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp&lt;/code&gt; 层，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnforceCursor()&lt;/code&gt; 是整个游戏的唯一光标控制入口。旧代码的典型模式如下(（被注释掉，没有实际生效）：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;LawnApp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EnforceCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mSEHOccured&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMouseIn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IDC_ARROW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mOverrideCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mOverrideCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_POINTER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetModuleHandle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAKEINTRESOURCE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;IDC_CURSOR1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)));&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_HAND&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mHandCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 更多 Windows 专属分支 ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码有两大问题：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;平台耦合过重&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HCURSOR&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadCursor&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetCursor&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetModuleHandle&lt;/code&gt; 全部是 Win32 API，无法在其他平台上编译或运行。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;架构位置过高&lt;/strong&gt;：光标作为通用窗口系统能力，本应由应用框架层（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SexyAppBase&lt;/code&gt;）负责，而不应该由游戏逻辑层（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp&lt;/code&gt;）持有。将光标控制放在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp&lt;/code&gt; 意味着每个基于该框架的新项目都要重复实现一遍。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在 PvZ-Portable 此前的版本中，这段代码一直处于被完全注释掉的状态，导致跨平台构建时虽然能编译通过，但光标行为是缺失的。&lt;/p&gt;

&lt;h2 id=&quot;跨平台重构下沉到-sexyappbase&quot;&gt;跨平台重构：下沉到 SexyAppBase&lt;/h2&gt;

&lt;p&gt;修复的第一步是&lt;strong&gt;把光标逻辑从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp&lt;/code&gt; 完全移除&lt;/strong&gt;，转而在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SexyAppBase&lt;/code&gt; 中以 SDL API 重新实现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnforceCursor()&lt;/code&gt;。这样，所有基于该框架的应用都能自动获得跨平台光标支持。&lt;/p&gt;

&lt;p&gt;新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnforceCursor()&lt;/code&gt; 核心逻辑如下：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SexyAppBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;EnforceCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSEHOccured&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_POINTER&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NUM_CURSORS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_POINTER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_NONE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;SDL_ShowCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDL_DISABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 1. 优先使用自定义光标（如果启用且已设置图片）&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mCustomCursorsEnabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCursorImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... 从 MemoryImage 创建或命中缓存 ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 2. 回退到系统光标&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCachedCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSysCursors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCachedCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aCachedCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_CreateSystemCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CursorNumToSystemCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCachedCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_GetDefaultCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;SDL_SetCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;SDL_ShowCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDL_ENABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个结构清晰地划分了三个层级：&lt;strong&gt;自定义光标 → 系统光标 → 默认光标&lt;/strong&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_NONE&lt;/code&gt; 则单独走隐藏分支，确保在任何平台下都能可靠地隐藏鼠标指针。&lt;/p&gt;

&lt;h2 id=&quot;系统光标映射&quot;&gt;系统光标映射&lt;/h2&gt;

&lt;p&gt;SDL2 提供了一套与操作系统无关的系统光标枚举 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_SystemCursor&lt;/code&gt;。笔者编写了一个静态映射函数，将引擎内部使用的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_xxx&lt;/code&gt; 枚举转换为对应的 SDL 枚举：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SystemCursor&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;CursorNumToSystemCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_HAND&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_HAND&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_TEXT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_IBEAM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_CIRCLE_SLASH&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_NO&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_SIZEALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;     &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_SIZEALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_SIZENESW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_SIZENESW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_SIZENS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_SIZENS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_SIZENWSE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_SIZENWSE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_SIZEWE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_SIZEWE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_WAIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_WAIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_DRAGGING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_POINTER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_NONE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_CUSTOM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;default:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_SYSTEM_CURSOR_ARROW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里有几个值得注意的设计选择：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_DRAGGING&lt;/code&gt; 映射到箭头&lt;/strong&gt;：SDL 并没有专门的”拖动中”系统光标，因此回退到标准箭头是合理的。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_CUSTOM&lt;/code&gt; 也映射到箭头&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_CUSTOM&lt;/code&gt; 的语义是”使用开发者通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetCursorImage&lt;/code&gt; 设置的自定义图片”。如果自定义图片未设置或未启用，回退到箭头光标能避免光标突然消失。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;懒加载（Lazy Initialization）&lt;/strong&gt;：系统光标只在第一次需要时通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateSystemCursor&lt;/code&gt; 创建，并缓存到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mSysCursors&lt;/code&gt; 数组中。这避免了在应用启动时就为所有平台创建大量可能永远用不到的光标句柄。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;从-memoryimage-到-sdl_cursor&quot;&gt;从 MemoryImage 到 SDL_Cursor&lt;/h2&gt;

&lt;p&gt;相比系统光标，自定义光标的实现要复杂得多。PvZ-Portable 的图像系统使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemoryImage&lt;/code&gt; 作为内存位图的抽象，其像素数据以 BGRA32 格式存储在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mBits&lt;/code&gt; 指针中。而 SDL 创建自定义光标需要 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_Surface&lt;/code&gt;。因此，关键问题是如何&lt;strong&gt;在不复制像素数据的前提下，将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemoryImage&lt;/code&gt; 包装为 SDL 表面&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;笔者选择了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateRGBSurfaceWithFormatFrom&lt;/code&gt;，它允许直接从现有的像素缓冲区创建表面，而无需额外拷贝：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;CreateCursorFromMemoryImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MemoryImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mBits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aWidth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHeight&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aWidth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHeight&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;SDL_Surface&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSurface&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_CreateRGBSurfaceWithFormatFrom&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mBits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aWidth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aHeight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aWidth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)),&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;SDL_PIXELFORMAT_BGRA32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSurface&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_CreateColorCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSurface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;SDL_FreeSurface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSurface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码的技术细节包括：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;零拷贝创建 Surface&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateRGBSurfaceWithFormatFrom&lt;/code&gt; 直接使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;theImage-&amp;gt;mBits&lt;/code&gt; 作为像素源，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_FreeSurface&lt;/code&gt; 时不会释放这个外部缓冲区，因此 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemoryImage&lt;/code&gt; 的生命周期不受影响。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;像素格式对齐&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemoryImage&lt;/code&gt; 内部使用 32 位 BGRA，pitch 为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;width * sizeof(uint32_t)&lt;/code&gt;，与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_PIXELFORMAT_BGRA32&lt;/code&gt; 完全匹配。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;热点坐标&lt;/strong&gt;：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateColorCursor&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(0, 0)&lt;/code&gt; 热点对于 PvZ-Portable 的自定义光标资源来说是适用的。如果未来需要更精细的热点控制，可以在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MemoryImage&lt;/code&gt; 或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Image&lt;/code&gt; 接口中扩展元数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;缓存策略与性能优化&quot;&gt;缓存策略与性能优化&lt;/h2&gt;

&lt;p&gt;自定义光标有一个显著的运行时开销：&lt;strong&gt;每次调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateColorCursor&lt;/code&gt; 都会分配新的操作系统光标资源&lt;/strong&gt;。如果在每一帧都重新创建，不仅会造成内存分配压力，还可能导致光标闪烁。因此，缓存机制必不可少。&lt;/p&gt;

&lt;p&gt;笔者设计了一个双层缓存策略：&lt;/p&gt;

&lt;h3 id=&quot;系统光标缓存&quot;&gt;系统光标缓存&lt;/h3&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// SexyAppBase.h&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSysCursors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NUM_CURSORS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mSysCursors&lt;/code&gt; 是一个固定大小的指针数组。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnforceCursor()&lt;/code&gt; 在首次需要某个系统光标时调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_CreateSystemCursor&lt;/code&gt;，并将结果缓存到对应槽位。后续再切换到同一光标时直接命中，无需与操作系统交互。这些缓存在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SexyAppBase&lt;/code&gt; 析构时统一 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_FreeCursor&lt;/code&gt; 释放。&lt;/p&gt;

&lt;h3 id=&quot;自定义光标缓存&quot;&gt;自定义光标缓存&lt;/h3&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// SexyAppBase.h&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;mCustomCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;mCustomCursorImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;          &lt;span class=&quot;n&quot;&gt;mCustomCursorImageNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;自定义光标采用&lt;strong&gt;单例缓存&lt;/strong&gt;模式（而非数组），原因如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;资源开销更高&lt;/strong&gt;：自定义光标的创建需要遍历像素数据、生成表面、再生成系统光标对象，成本远高于系统光标。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;使用频率更低&lt;/strong&gt;：游戏中大部分光标都是系统光标（箭头、手型、等待等），自定义光标只在特定场景出现，通常一次只会使用一张自定义图片。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;缓存命中判定逻辑：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mCustomCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCustomCursorImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorImage&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCustomCursorImageNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCustomCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 命中缓存&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;SDL_Cursor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNewCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CreateCursorFromMemoryImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aMemoryImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aNewCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ResetCustomCursorCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 释放旧缓存&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mCustomCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNewCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mCustomCursorImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mCustomCursorImageNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aCursor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCustomCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;此外，如果上层逻辑通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SetCursorImage()&lt;/code&gt; 替换了某光标编号对应的图片，且该编号正好是当前缓存的自定义光标，则必须立即失效缓存，否则新图片不会生效：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SexyAppBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetCursorImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NUM_CURSORS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mCustomCursorImageNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCursorImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;ResetCustomCursorCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;mCursorImages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCursorNum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;EnforceCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnableCustomCursors&lt;/code&gt; 同样会在关闭自定义光标时清空缓存，避免在禁用状态下仍持有不必要的系统资源。&lt;/p&gt;

&lt;h2 id=&quot;隐藏光标与状态管理&quot;&gt;隐藏光标与状态管理&lt;/h2&gt;

&lt;p&gt;除了”显示什么光标”之外，”是否显示光标”同样重要。原版引擎使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_NONE&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_CUSTOM&lt;/code&gt; 两个枚举值来表达”不显示系统光标”。在新的 SDL 实现中，笔者对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_NONE&lt;/code&gt; 做了明确处理：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCursorNum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CURSOR_NONE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;SDL_ShowCursor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDL_DISABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;当光标编号为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_NONE&lt;/code&gt; 时，直接调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_ShowCursor(SDL_DISABLE)&lt;/code&gt; 隐藏鼠标指针，不再尝试创建或设置任何光标对象。这对于战斗场景、过场动画或全屏模式非常有用——玩家不希望一个巨大的箭头遮挡画面中心。&lt;/p&gt;

&lt;p&gt;与此同时，从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURSOR_NONE&lt;/code&gt; 切换回其他任何光标时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EnforceCursor()&lt;/code&gt; 的正常流程会在设置新光标后调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_ShowCursor(SDL_ENABLE)&lt;/code&gt;，确保指针重新出现。状态转换是可靠且可逆的。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;光标系统在游戏引擎中往往被视为”边缘功能”，但在跨平台移植的语境下，它其实是一个完整的子系统：从操作系统抽象、像素格式转换、资源生命周期管理到运行时缓存策略，每一个环节都需要仔细设计。&lt;/p&gt;

&lt;p&gt;通过这次重构，PvZ-Portable 彻底摆脱了 Win32 光标的遗留包袱，获得了真正意义上的跨平台光标支持。无论是桌面平台（Windows、Linux、macOS）、移动平台（通过外接鼠标），还是 WebAssembly 浏览器环境，玩家都能获得一致且完整的光标交互体验。&lt;/p&gt;

&lt;h2 id=&quot;️-版权与说明&quot;&gt;⚠️ 版权与说明&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;重要：本项目仅包含代码引擎，不包含任何游戏素材！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PvZ-Portable 严格遵守版权协议。游戏的 IP（植物大战僵尸）属于 PopCap/EA。&lt;/p&gt;

&lt;p&gt;要研究或使用此项目，你&lt;strong&gt;必须&lt;/strong&gt;拥有正版游戏（如果没有，请在 &lt;a href=&quot;https://store.steampowered.com/app/3590/Plants_vs_Zombies_GOTY_Edition/&quot;&gt;Steam&lt;/a&gt; 或 &lt;a href=&quot;https://www.ea.com/games/plants-vs-zombies/plants-vs-zombies&quot;&gt;EA 官网&lt;/a&gt; 上购买）。你需要从正版游戏中提取以下文件放到 PvZ-Portable 的程序所在目录中。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.pak&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties/&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本项目的源代码以 &lt;a href=&quot;https://www.gnu.org/licenses/lgpl-3.0.html&quot;&gt;&lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;&lt;/a&gt; 许可证开源，欢迎学习和贡献。&lt;/p&gt;
</description>
        <pubDate>Thu, 16 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/16/PvZ-Portable-SDL-Cursor-System/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/16/PvZ-Portable-SDL-Cursor-System/</guid>
        
        <category>C++</category>
        
        <category>SDL2</category>
        
        <category>游戏移植</category>
        
        <category>开源软件</category>
        
        <category>开源游戏</category>
        
        <category>PvZ-Portable</category>
        
        
      </item>
    
      <item>
        <title>PvZ-Portable：长期运行稳定性修复——从计数器溢出到浮点精度悬崖</title>
        <description>&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;在调试 PvZ-Portable 的过程中，笔者注意到一类只在&lt;strong&gt;长时间运行&lt;/strong&gt;后才会显现的异常：当无尽模式推进到数十小时后，某些周期性动画开始出现肉眼可见的抖动——浓雾的波动不再平滑，水生植物的漂浮动画变得不连续，就连 UI 元素的闪烁节奏也会偶发错乱。&lt;/p&gt;

&lt;p&gt;这些问题看似分布在不同子系统，其实根因只有一个：&lt;strong&gt;游戏计数器在不断累加后，首先会触碰到 IEEE 754 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 的精度问题；而 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 的 signed overflow 则是另一个隐患&lt;/strong&gt;。本文将记录笔者如何系统性地重构 PvZ-Portable 中的主计数器与动画相位计算，改进超长期运行的稳定性。&lt;/p&gt;

&lt;h2 id=&quot;浮点精度&quot;&gt;浮点精度&lt;/h2&gt;

&lt;p&gt;PvZ-Portable 的动画系统大量使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 来计算浓雾波动、弹坑水波、漂浮植物的正弦运动等周期性相位。这些计算的典型模式是：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;200.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aWave&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aPos&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt; 是每帧递增的整数计数器。问题在于，当这个大整数被转成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 并乘以一个系数后，IEEE 754 单精度浮点有限的 &lt;strong&gt;24 位有效尾数&lt;/strong&gt; 会让相位的增量逐渐丧失精度。&lt;/p&gt;

&lt;p&gt;以漂浮植物为例，每帧增量约为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2π/200 ≈ 0.0314&lt;/code&gt;。当 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt; 增长到约 &lt;strong&gt;1670 万&lt;/strong&gt; 时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 在该数量级下的相邻可表示值间距（ULP）会超过这个增量。这意味着相邻两帧的相位输入不再有区别，正弦函数的参数被卡住在更粗粒的近似值上，原本平滑的正弦波会退化为阶梯状抖动。按逻辑帧率 100 FPS 计算，这个临界点出现在运行约 &lt;strong&gt;46 小时&lt;/strong&gt; 之后。&lt;/p&gt;

&lt;p&gt;不同动画的缩放系数不同，触达的具体帧数略有差异，但都集中在 &lt;strong&gt;40～50 小时&lt;/strong&gt; 这个区间。对于不停使用同一局存档的无尽模式玩家来说，这完全可能在正常游戏中触发。&lt;/p&gt;

&lt;h3 id=&quot;精度衰减时间表&quot;&gt;精度衰减时间表&lt;/h3&gt;

&lt;p&gt;我们可以把漂浮植物当作典型案例，算一算它会如何随时间逐步坏掉。它的相位增量是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2π/200 ≈ 0.0314&lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 的相邻可表示值间距（ULP）在不同数量级下如下：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;运行时长&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt;&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 的 ULP&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;等价效果&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;~46 小时&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.67×10⁷&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0.0625&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;2 帧&lt;/strong&gt; 相位才更新一次，开始出现轻微顿挫&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;~93 小时&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3.34×10⁷&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0.125&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;4 帧&lt;/strong&gt; 一跳，肉眼可见的抽搐&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;~7.7 天&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.34×10⁸&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;32 帧&lt;/strong&gt;（0.3 秒）一跳，已经完全不像平滑动画&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;~62 天&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;5.34×10⁸&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;4.0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;128 帧&lt;/strong&gt;（1.3 秒）一跳，像幻灯片&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;~248 天&lt;/strong&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32&lt;/code&gt; 溢出前）&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2.15×10⁹&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;8.0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;255 帧&lt;/strong&gt;（2.5 秒）才抽搐一下&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;也就是说，如果一个玩家真的把游戏挂到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 溢出前夕，荷叶每隔两秒半才会晃动一下。更糟糕的是，第 249 天溢出发生时，有符号 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt; 的 UB 会让相位瞬间跳变成一个完全不可预测的值——即使它此前勉强还能运行，此时也可能行为错乱。&lt;/p&gt;

&lt;h2 id=&quot;int32_t-溢出的未定义行为&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 溢出的未定义行为&lt;/h2&gt;

&lt;p&gt;原版引擎使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt;（即 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;）存储 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mAppCounter&lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 的上限约为 21.47 亿，按 100 FPS 计算约 &lt;strong&gt;248 天&lt;/strong&gt; 会触及溢出边界。在 C++ 中，&lt;strong&gt;有符号整数溢出是未定义行为&lt;/strong&gt;，编译器可以据此做出任何假设，导致行为不可靠。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abs(INT_MIN)&lt;/code&gt; 在 C++ 中同样是 UB。旧代码里 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter / 8 % 22 - 11&lt;/code&gt; 的实际范围不可能触及 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INT_MIN&lt;/code&gt;，但计数器类型一旦向无符号迁移，类似表达式的语义就会变得更危险：无符号减法下溢会得到一个巨大的正数，此时再套一层 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abs()&lt;/code&gt; 就存在 UB 风险。&lt;/p&gt;

&lt;h2 id=&quot;计数器类型重构&quot;&gt;计数器类型重构&lt;/h2&gt;

&lt;p&gt;明确了这两个根因后，修复路径就很清晰了：先把所有计数器统一为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;，再在送入 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 之前对大周期取模。&lt;/p&gt;

&lt;h3 id=&quot;将主计数器切换为-uint32_t&quot;&gt;将主计数器切换为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;修复的第一步是将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; 类）和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mAppCounter&lt;/code&gt;（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LawnApp&lt;/code&gt; 类）从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 改为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;旧代码：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Board.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// LawnApp.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mAppCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修复后：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Board.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// LawnApp.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mAppCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;无符号整数的溢出在 C++ 中是&lt;strong&gt;定义良好的模 2^32 回绕行为&lt;/strong&gt;。这不仅彻底消除了 UB 风险，还顺带解决了一些工程上的麻烦：存档同步、跨平台联机、以及时间比较逻辑都因此获得了稳定的行为基线。数值周期性运行，配合前面提到的动画相位取模，回绕到 0 不会产生任何错误——计数器进入了任意时间尺度都&lt;strong&gt;定义正确&lt;/strong&gt;的运行状态。&lt;/p&gt;

&lt;h3 id=&quot;统一派生计数器&quot;&gt;统一派生计数器&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board&lt;/code&gt; 中还存在着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mEffectCounter&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mDrawCount&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mIntervalDrawCountStart&lt;/code&gt;，以及 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PoolEffect&lt;/code&gt; 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mPoolCounter&lt;/code&gt;。这些计数器同样参与周期性动画或时间间隔计算，必须保持一致的类型语义。笔者将它们全部统一为无符号类型：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Board.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mEffectCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mDrawCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mIntervalDrawCountStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// PoolEffect.h&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mPoolCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;存档同步路径的适配&quot;&gt;存档同步路径的适配&lt;/h3&gt;

&lt;p&gt;PvZ-Portable 使用自定义的 Portable v4 存档格式。计数器类型变更后，序列化路径必须同步切换为无符号读写。&lt;/p&gt;

&lt;p&gt;修改前：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SyncInt32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theBoard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SyncInt32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theBoard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mEffectCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改后：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SyncUInt32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theBoard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SyncUInt32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theBoard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mEffectCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这确保了旧存档的兼容性不会被破坏（位模式不变，只是解释方式明确为无符号），同时消除了读档时可能的符号扩展歧义。&lt;/p&gt;

&lt;h2 id=&quot;动画相位的周期性取模&quot;&gt;动画相位的周期性取模&lt;/h2&gt;

&lt;p&gt;计数器类型改对了，但这只消除了溢出层面的风险。但无论计数器怎么改，把它直接喂给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 做周期运算，还是因精度问题而出现抖动。真正的办法是，&lt;strong&gt;在向 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 转换之前先对大周期取模&lt;/strong&gt;，确保输入三角函数的数值始终处于安全精度范围内。&lt;/p&gt;

&lt;h3 id=&quot;浓雾动画fog&quot;&gt;浓雾动画（Fog）&lt;/h3&gt;

&lt;p&gt;浓雾波动由两个周期参数共同决定（900 帧与 500 帧），其最小公倍数为 4500 帧。修复后的代码在转换前先对 4500 取模：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;constexpr&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FOG_ANIM_PERIOD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;FOG_ANIM_PERIOD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这保证无论游戏运行多久，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aTime&lt;/code&gt; 的绝对值都不会超过 4500 × 2π ≈ 28274，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 的精度范围内游刃有余。&lt;/p&gt;

&lt;h3 id=&quot;弹坑水波crater&quot;&gt;弹坑水波（Crater）&lt;/h3&gt;

&lt;p&gt;弹坑在泳池场景下的水波摆动周期为 200 帧：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;constexpr&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CRATER_ANIM_PERIOD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CRATER_ANIM_PERIOD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
              &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CRATER_ANIM_PERIOD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;宝石迷阵旋转效果&quot;&gt;宝石迷阵旋转效果&lt;/h3&gt;

&lt;p&gt;宝石迷阵模式中的扭转动画每 1000 帧旋转一周：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;constexpr&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BEGHOULED_TWIST_ROTATION_PERIOD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aRotation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mBoard&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BEGHOULED_TWIST_ROTATION_PERIOD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.001&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;漂浮植物与咖啡豆&quot;&gt;漂浮植物与咖啡豆&lt;/h3&gt;

&lt;p&gt;水生植物的漂浮效果，以及咖啡豆的上下波动，也都采用了类似的取模策略。&lt;/p&gt;

&lt;p&gt;旧代码直接将整数计数器乘以系数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aCounter * 0.03f&lt;/code&gt;。修复后改用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmod&lt;/code&gt; 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;double&lt;/code&gt; 精度下先对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2π&lt;/code&gt; 取模，再降级为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fmod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mRow&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;97.0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mPlantCol&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;61.0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;double&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.03&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
         &lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里要特别小心，因为咖啡豆的相位计算不仅依赖计数器，还叠加了行和列的固定偏移，数值增长更快。&lt;/p&gt;

&lt;h3 id=&quot;泳池波动pooleffect&quot;&gt;泳池波动（PoolEffect）&lt;/h3&gt;

&lt;p&gt;泳池的水面波纹由多组不同周期的正弦波叠加而成，涉及的周期包括 1600、300、1800、220、3200/3、200、720、640、88 帧等。笔者计算出这些周期的共同周期边界：&lt;strong&gt;316800 帧&lt;/strong&gt;，作为主周期取模边界：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;constexpr&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;POOL_PHASE_PERIOD&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;316800u&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aPoolPhase&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mPoolCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;POOL_PHASE_PERIOD&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;同时，PoolEffect 中负责计算焦散纹理的固定点代码里，原先使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt; 存储的位移量也被明确为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsigned int&lt;/code&gt;，避免了左移运算在 signed 类型上的 UB 风险：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 旧代码&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeU&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timePool0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mPoolCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 修复后&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timeU&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timePool0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mPoolCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;闪烁颜色与-abs-的安全性&quot;&gt;闪烁颜色与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abs&lt;/code&gt; 的安全性&lt;/h2&gt;

&lt;p&gt;动画相位的问题解决了，计数器类型变更还牵出了一处接口层面的细枝末节。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetFlashingColor&lt;/code&gt; 是游戏中广泛使用的闪烁/高亮颜色插值函数，原先接收 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int theCounter&lt;/code&gt;。当参数改为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt; 后，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;theCounter % theFlashTime&lt;/code&gt; 变为无符号取模，随后与有符号的中间值相减可能引发符号混乱。&lt;/p&gt;

&lt;p&gt;修改前：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;Color&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GetFlashingColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theFlashTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTimeAge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theFlashTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... abs(aTimeInf - aTimeAge) ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改后：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;Color&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GetFlashingColor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCounter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theFlashTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTimeAge&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32_t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theFlashTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;另一处风险在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Board::DrawUIBottom&lt;/code&gt; 中。旧代码：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aWaveTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;22&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;迁移到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt; 后，如果不做显式 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;static_cast&amp;lt;int&amp;gt;&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter / 8 % 22 - 11&lt;/code&gt; 会先进行无符号运算，结果小于 11 时会发生下溢，变成一个巨大的正数。修复后：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aWaveTime&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMainCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;22&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样不但避免了无符号下溢，也彻底消除了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;abs(INT_MIN)&lt;/code&gt; 的 UB 隐患。&lt;/p&gt;

&lt;h2 id=&quot;数据一览&quot;&gt;数据一览&lt;/h2&gt;

&lt;p&gt;下表汇总了这次修复涉及的关键计数器及其长期运行特性：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;计数器&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;旧类型&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;新类型&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;100 FPS 下的环绕/溢出周期&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mMainCounter&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;497 天&lt;/strong&gt;（定义良好）&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mAppCounter&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;497 天&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mPoolCounter&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unsigned int&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;497 天&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mEffectCounter&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;497 天&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mDrawCount&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;497 天&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;而浮点精度问题在取模修复前的表现为：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;动画效果&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;缩放系数&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;出现可见抖动的预估时长（100 FPS）&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;漂浮植物&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2π/200&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;46 小时&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;浓雾波动&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2π&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;46～47 小时&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;咖啡豆浮动&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.03&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;48 小时&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;泳池波纹&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;π&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;约 &lt;strong&gt;46 小时&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;现在，所有计数器都改为无符号类型，即使无限递增也不会触发 UB；同时，所有动画的相位输入都被限制在一个安全的周期范围内，避免了长期运行的精度问题。&lt;/p&gt;

&lt;h3 id=&quot;回绕对齐的缺陷&quot;&gt;回绕对齐的缺陷&lt;/h3&gt;

&lt;p&gt;然而，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt; 的自然回绕周期 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2^32 = 4294967296&lt;/code&gt; 与各动画的取模周期&lt;strong&gt;并不保证对齐&lt;/strong&gt;。这意味着在约 497 天后的回绕瞬间，理论上会出现一次相位跳变。&lt;/p&gt;

&lt;p&gt;举个例子，漂浮植物的周期是 200 帧。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4294967296 % 200 = 96&lt;/code&gt;，所以回绕时 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;% 200&lt;/code&gt; 的结果会从 95 直接跳到 0，而不是正常地走完 96→199。这相当于半个周期的瞬时跳变——严格来说，水生植物上下浮动位置会在那一帧突然跳变。&lt;/p&gt;

&lt;p&gt;同理，浓雾的 4500 帧周期也不整除 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2^32&lt;/code&gt;（余数为 796），回绕时同样存在一次跳变。&lt;/p&gt;

&lt;p&gt;要彻底消除这个跳变，理论上需要让计数器在所有动画周期的&lt;strong&gt;公倍数&lt;/strong&gt;处回绕（比如 {4500, 200, 1000, 316800} 的最小公倍数是 792000 帧），或者让所有动画周期都整除 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2^32&lt;/code&gt;——前者会破坏计数器用于非动画逻辑时的单调性，后者对含有因数 3、5 的周期（如 4500）根本不可能。&lt;/p&gt;

&lt;p&gt;所幸，这个跳变只发生在&lt;strong&gt;连续运行 497 天后的一帧&lt;/strong&gt;，且不会造成额外的任何破坏。与其引入复杂的全局同步机制，不如坦然接受这个理论上存在、实践中几乎不可见的瑕疵。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;这次修复的核心启示非常直观，却又极易被忽视：&lt;strong&gt;不要在热路径中将单调递增的大整数直接塞给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;float&lt;/code&gt; 做周期性运算&lt;/strong&gt;。对于以帧为单位计时的游戏引擎，24 位浮点尾数是一座真实存在的悬崖——运行约 46 小时后，它就会把平滑的动画变成一顿一顿的抖动。&lt;/p&gt;

&lt;p&gt;与此同时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;int32_t&lt;/code&gt; 的 signed overflow 在 C++ 中不是什么回绕到负数那么简单，它是实实在在的未定义行为。将计数器家族统一迁移到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uint32_t&lt;/code&gt;，不仅修复了 UB，还顺带获得了可预期的环绕语义和跨平台一致性。&lt;/p&gt;

&lt;p&gt;PvZ-Portable 作为开源重实现，在处理这类长期运行假设时尤其需要谨慎。原版游戏很多设计可能并不精细，但现代平台上的开源重实现没有理由继承这种隐式的寿命限制。把计数器类型做对、把相位取模做对，才能确保玩家在任何时长下都能获得稳定体验。&lt;/p&gt;

&lt;h2 id=&quot;️-版权与说明&quot;&gt;⚠️ 版权与说明&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;重要：本项目仅包含代码引擎，不包含任何游戏素材！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PvZ-Portable 严格遵守版权协议。游戏的 IP（植物大战僵尸）属于 PopCap/EA。&lt;/p&gt;

&lt;p&gt;要研究或使用此项目，你&lt;strong&gt;必须&lt;/strong&gt;拥有正版游戏（如果没有，请在 &lt;a href=&quot;https://store.steampowered.com/app/3590/Plants_vs_Zombies_GOTY_Edition/&quot;&gt;Steam&lt;/a&gt; 或 &lt;a href=&quot;https://www.ea.com/games/plants-vs-zombies/plants-vs-zombies&quot;&gt;EA 官网&lt;/a&gt; 上购买）。你需要从正版游戏中提取以下文件放到 PvZ-Portable 的程序所在目录中。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.pak&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties/&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本项目的源代码以 &lt;a href=&quot;https://www.gnu.org/licenses/lgpl-3.0.html&quot;&gt;&lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;&lt;/a&gt; 许可证开源，欢迎学习和贡献。&lt;/p&gt;
</description>
        <pubDate>Mon, 13 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/13/PvZ-Portable-Long-Run-Counter-Stability/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/13/PvZ-Portable-Long-Run-Counter-Stability/</guid>
        
        <category>C++</category>
        
        <category>游戏移植</category>
        
        <category>开源软件</category>
        
        <category>开源游戏</category>
        
        <category>PvZ-Portable</category>
        
        
      </item>
    
      <item>
        <title>PvZ-Portable 资源释放时序安全：动画附件生命周期与 SDL 音频回调的硬防护</title>
        <description>&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;在 PvZ-Portable 的开发和测试过程中，笔者遇到一类非常棘手的崩溃：它们不发生在游戏逻辑最繁忙的更新阶段，而是在某个对象被销毁、资源被释放的&lt;strong&gt;瞬间&lt;/strong&gt;触发。这类崩溃的共同特征是——对象已经开始析构，但外部系统仍在通过指针或回调异步访问它。&lt;/p&gt;

&lt;p&gt;本文将记录两个典型的修复：一是 &lt;strong&gt;Reanimator 动画附件系统的生命周期治理&lt;/strong&gt;，解决特定僵尸动画在加载和卸载时的指针混乱与悬空引用；二是 &lt;strong&gt;SDL_mixer 音频实例的析构时序加固&lt;/strong&gt;，防止音效回调在声音对象已释放后继续触碰悬垂内存。这两个子系统截然不同，但指向同一个底层问题：&lt;strong&gt;手动管理内存的 C++ 对象与外部 C 风格 API 之间的生命周期耦合&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&quot;reanimator-动画附件的生命周期治理&quot;&gt;Reanimator 动画附件的生命周期治理&lt;/h2&gt;

&lt;h3 id=&quot;崩溃现象与排查方向&quot;&gt;崩溃现象与排查方向&lt;/h3&gt;

&lt;p&gt;最早触发问题的是投石车僵尸（Catapult Zombie）。在特定状态下切换贴图时，或者当包含该僵尸的关卡结束、动画资源被回收时，引擎会概率性崩溃。堆栈指向 Reanimator 的绘制路径或附件销毁路径，很容易让人误以为是图片资源本身的损坏或释放过早。&lt;/p&gt;

&lt;p&gt;深入排查后，笔者发现问题的根源并不在资源加载器，而在 &lt;strong&gt;Reanimator 的 Atlas 语义与附件的释放约定&lt;/strong&gt; 上。这两个层面的设计彼此交织，错误的假设一路传导，最终在高负载或对象销毁时被放大成崩溃。&lt;/p&gt;

&lt;h3 id=&quot;atlas-语义修正区分编码句柄与真实指针&quot;&gt;Atlas 语义修正：区分编码句柄与真实指针&lt;/h3&gt;

&lt;p&gt;PvZ-Portable 的重实现支持 Reanim Atlas，这是一种将多张贴图合并到一张大图集上的优化机制。在 Atlas 模式下，动画帧数据中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mImage&lt;/code&gt; 字段存储的并不是可以直接绘制的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Image*&lt;/code&gt; 指针，而是一个&lt;strong&gt;编码后的 Atlas 句柄&lt;/strong&gt;。绘制时需要通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetEncodedReanimAtlas&lt;/code&gt; 解码，才能得到真正的贴图信息和子图坐标。&lt;/p&gt;

&lt;p&gt;旧代码在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Reanimation::DrawTrack&lt;/code&gt; 中对这流程的处理存在逻辑漏洞：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 旧代码（简化后）&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTransform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mDefinition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mReanimAtlas&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ReanimAtlasImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mDefinition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mReanimAtlas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetEncodedReanimAtlas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mOriginalImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aTrackInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImageOverride&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码有两大问题：&lt;/p&gt;

&lt;p&gt;第一，&lt;strong&gt;解码顺序错误&lt;/strong&gt;。当某个 track 存在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mImageOverride&lt;/code&gt;（显示覆盖贴图）时，旧代码会先尝试将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 当作 Atlas 句柄解码，然后再发现存在 override 并丢弃 Atlas 结果。这个顺序本身不致命，但它意味着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 在没有 override 且不是合法 Atlas 句柄时，会被当作 raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Image*&lt;/code&gt; 使用——而实际上，如果 Atlas 存在，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 可能只是编码值，根本不是一个稳定可用的指针。&lt;/p&gt;

&lt;p&gt;第二，&lt;strong&gt;对非 Atlas 图片的误判&lt;/strong&gt;。旧代码的判定是：Atlas 存在且 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage != nullptr&lt;/code&gt;，就进入 Atlas 解码分支。但逻辑上应该区分轨道变换自带的 frame image（可能是编码句柄）和显式指定的非 Atlas 覆盖图。如果开发者或游戏逻辑为某个 track 设置了一张普通的非 Atlas 图片作为 override，旧代码仍可能在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 上执行 Atlas 解码，得到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullptr&lt;/code&gt; 后又错误地把它当成空贴图处理。&lt;/p&gt;

&lt;p&gt;修复后的逻辑彻底重写了分支结构：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 修复后&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTransform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ReanimAtlasImage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mDefinition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mReanimAtlas&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mDefinition&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mReanimAtlas&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetEncodedReanimAtlas&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aTrackInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImageOverride&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTrackInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImageOverride&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aAtlasImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 不合法的编码句柄，不能当作 raw Image* 使用&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aTrackInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImageOverride&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;nullptr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTrackInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mImageOverride&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;修改后的关键变化是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ImageOverride 优先级最高&lt;/strong&gt;。如果存在覆盖贴图，直接跳过 Atlas 路径，不再对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 做任何假设性的解码。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;严格区分 Atlas 编码句柄与 raw 指针&lt;/strong&gt;。如果 Atlas 存在但解码失败，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 会被显式置为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nullptr&lt;/code&gt;，杜绝将非法编码值当作普通指针解引用的风险。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;绘制时的矩阵计算也分层处理&lt;/strong&gt;。如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aAtlasImage&lt;/code&gt; 有效，使用 Atlas 的元数据计算 pivot；否则使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aImage&lt;/code&gt; 自身的尺寸信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;同一修复还同步应用于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetCurrentTrackImage&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetTrackMatrix&lt;/code&gt;，确保查询和绘制两条路径的语义一致。&lt;/p&gt;

&lt;h3 id=&quot;附件系统的释放&quot;&gt;附件系统的释放&lt;/h3&gt;

&lt;p&gt;解决了 Atlas 语义问题后，另一类崩溃出现在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Attachment&lt;/code&gt; 系统的释放路径中。&lt;/p&gt;

&lt;p&gt;在 PvZ-Portable 中，Attachment 是一种挂载在骨骼动画轨道上的特效容器。一个 Attachment 可以包含粒子系统（ParticleSystem）、拖尾（Trail）、子动画（Reanimation）甚至嵌套 Attachment。这些子资源的生命周期由 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EffectSystem&lt;/code&gt; 统一管理，而 Attachment 本身则由 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AttachmentHolder&lt;/code&gt; 通过一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataArray&lt;/code&gt; 分配和回收。&lt;/p&gt;

&lt;p&gt;旧代码中存在两个互相加剧的问题：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;问题一：Detach 后没有清理条目。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AttachmentDetachCrossFadeParticleType&lt;/code&gt; 用于在取消挂载某种粒子特效时，通知粒子系统进入 cross-fade 或死亡状态。旧代码的逻辑大概是：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mNumEffects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;找到匹配的粒子&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCrossFadeName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aParticleSystem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CrossFade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCrossFadeName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aParticleSystem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ParticleSystemDie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码&lt;strong&gt;只通知了粒子系统死亡&lt;/strong&gt;，却没有将对应的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AttachEffect&lt;/code&gt; 条目从 Attachment 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mEffectArray&lt;/code&gt; 中移除。结果是：粒子系统消亡后，Attachment 仍然保留着一个指向已死粒子系统的 ID。当后续代码遍历该 Attachment 的效果数组时，会拿到一个无效 ID，进而解引用一个已释放或已标记为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mDead&lt;/code&gt; 的对象。&lt;/p&gt;

&lt;p&gt;修复后的逻辑在通知粒子死亡的同时，&lt;strong&gt;立即将条目从数组中移除&lt;/strong&gt;：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mNumEffects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;AttachEffect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAttachEffect&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mEffectArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 匹配粒子逻辑 ...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;匹配成功&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCrossFadeName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aParticleSystem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CrossFade&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCrossFadeName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;aParticleSystem&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ParticleSystemDie&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 立即从数组中移除该条目&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNumEffectsRemaining&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mNumEffects&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aNumEffectsRemaining&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;memmove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aAttachEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aAttachEffect&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNumEffectsRemaining&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AttachEffect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mNumEffects&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mNumEffects&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aAttachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mDead&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;theAttachmentID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AttachmentID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ATTACHMENTID_NULL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样，Attachment 的所有权状态与底层粒子系统的实际生命周期始终保持一致。当最后一个 effect 被移除时，Attachment 自身也会立即被标记为死亡，不再被误用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;问题二：没有主动回收死附件。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AttachmentHolder::AllocAttachment&lt;/code&gt; 负责从 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataArray&lt;/code&gt; 中分配新的 Attachment。旧代码直接调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataArrayAlloc&lt;/code&gt;，没有任何容量满时的回收机制。如果 DataArray 接近容量上限，分配会失败，或者更糟糕的是——分配器可能复用一个尚未正确清理的槽位，导致新对象带着旧附件的残留数据。&lt;/p&gt;

&lt;p&gt;修复方案是在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllocAttachment&lt;/code&gt; 中引入预分配时的垃圾回收：当数组快要满时，先遍历所有现有 Attachment，调用新增的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PruneDeadEffects&lt;/code&gt; 函数清理已死亡的 effects，然后释放掉所有已经完全死亡的 Attachment。&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;Attachment&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;AttachmentHolder&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocAttachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mAttachments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mSize&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mAttachments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mMaxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... 遍历所有 attachment，执行 PruneDeadEffects ...&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... 将 mDead 的 attachment ID 收集到数组 ...&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... 逐个调用 DataArrayFree 释放死附件 ...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mAttachments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DataArrayAlloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PruneDeadEffects&lt;/code&gt; 是一个集中式的状态清理器。它会检查 Attachment 上的每一个 effect，若对应的底层对象（粒子、Trail、Reanim、子 Attachment）已经死亡或无效，就通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memmove&lt;/code&gt; 将其从紧凑数组中移除。这个函数既在分配压力的垃圾回收时被调用，也可以在未来需要时作为维护接口使用。&lt;/p&gt;

&lt;h3 id=&quot;投石车僵尸的调用侧修正&quot;&gt;投石车僵尸的调用侧修正&lt;/h3&gt;

&lt;p&gt;在 Atlas 语义修正的同时，笔者还修复了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Zombie::UpdateReanim&lt;/code&gt; 中投石车僵尸对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetCurrentTrackImage&lt;/code&gt; 的调用：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 旧代码&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aPoleImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aReanim&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GetCurrentTrackImage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Zombie_catapult_pole&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aPoleImage&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IMAGE_REANIM_ZOMBIE_CATAPULT_POLE_WITHBALL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSummonCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aReanim&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetImageOverride&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码试图通过查询当前轨道的贴图来判断投石车是否还载着篮球。但 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GetCurrentTrackImage&lt;/code&gt; 在 Atlas 模式下返回的是不稳定的气候编码句柄，用它和特定的常量 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Image*&lt;/code&gt; 做等值比较本身就是不可靠的。正确的逻辑应当直接依赖 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mSummonCounter&lt;/code&gt; 这个已被计算好的状态变量：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 修复后&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mSummonCounter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;aReanim&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetImageOverride&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Zombie_catapult_pole&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;IMAGE_REANIM_ZOMBIE_CATAPULT_POLE_DAMAGE_WITHBALL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;不仅消除了 Atlas 句柄与 raw 指针比较带来的未定义行为，也让状态转换意图更加清晰。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;这里插入一个关于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceManager&lt;/code&gt; 的附带修复。在审查资源加载代码时，笔者为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ParseImageResource&lt;/code&gt; 中 sprite sheet 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rows&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cols&lt;/code&gt; 属性增加了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TOD_ASSERT&lt;/code&gt; 正向边界检查。这是一个防御性措施：如果资源配置文件因为格式错误或版本不一致而写入非正数，引擎可以在加载阶段就断言失败，而不是将零或负数带入后续的纹理切分计算中导致更隐蔽的崩溃。&lt;/p&gt;

&lt;h2 id=&quot;sdl-音频回调与对象析构的时序博弈&quot;&gt;SDL 音频回调与对象析构的时序博弈&lt;/h2&gt;

&lt;p&gt;另一类崩溃出现在音效密集播放的场景：当僵尸数量很多、豌豆射手和西瓜投手同时开火时，游戏概率性 segfault，堆栈往往指向 SDL_mixer 的某个 effect callback 内部。&lt;/p&gt;

&lt;p&gt;这个问题与动画附件系统的崩溃在表面上毫无关联，但究其本质，仍然是&lt;strong&gt;对象已经释放，外部回调还在继续访问它&lt;/strong&gt;。&lt;/p&gt;

&lt;h3 id=&quot;通道所有权的缺失&quot;&gt;通道所有权的缺失&lt;/h3&gt;

&lt;p&gt;PvZ-Portable 使用 SDL_mixer 作为音频后端。音效播放的流程大致是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundManager&lt;/code&gt; 维护一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_Chunk&lt;/code&gt; 数组（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mSourceSounds&lt;/code&gt;）和一个固定大小的通道池（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mPlayingSounds&lt;/code&gt;）。&lt;/li&gt;
  &lt;li&gt;当需要播放某个音效时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundManager&lt;/code&gt; 找到一个空闲通道，创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 对象，然后调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_PlayChannel&lt;/code&gt; 开始播放。&lt;/li&gt;
  &lt;li&gt;为了支持音高变化（pitch），代码在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_PlayChannel&lt;/code&gt; 之后通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_RegisterEffect&lt;/code&gt; 注册了一个回调函数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PitchHandlerFuncCallback&lt;/code&gt;，该回调会在 SDL_mixer 的混音线程中定期被调用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;旧代码中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 构造如下：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theSoundManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Mix_Chunk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theSourceSound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;播放时调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_PlayChannel(-1, ...)&lt;/code&gt; 让 SDL_mixer 自动分配通道。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mChannel&lt;/code&gt; 只是被动记录 SDL_mixer 返回的通道号。这里的关键问题是：&lt;strong&gt;SDLSoundInstance 并不知道自己被绑定到了哪个通道&lt;/strong&gt;。虽然 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundManager&lt;/code&gt; 在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mPlayingSounds&lt;/code&gt; 数组中以通道号为索引存放了实例指针，但实例对象本身并没有保留这个绑定关系。&lt;/p&gt;

&lt;h3 id=&quot;析构时序的竞争&quot;&gt;析构时序的竞争&lt;/h3&gt;

&lt;p&gt;更致命的是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 的析构函数在旧代码中是&lt;strong&gt;完全空的&lt;/strong&gt;：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::~&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 什么都不做&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这意味着，如果某个音效实例因为以下原因被销毁：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;自动释放逻辑（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mAutoRelease&lt;/code&gt;）在播放结束后触发；&lt;/li&gt;
  &lt;li&gt;上层逻辑主动调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Release()&lt;/code&gt;；&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundManager&lt;/code&gt; 在清理时覆盖该通道；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;那么 SDL_mixer 的混音线程仍可能在下一个 audio callback 中调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PitchHandlerFuncCallback(mChannel, ...)&lt;/code&gt;，而这个回调需要读取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 的成员 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mPitchHandler&lt;/code&gt;。一旦实例已被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt;，这就是典型的 use-after-free。&lt;/p&gt;

&lt;h3 id=&quot;修复斩断回调先于停止播放&quot;&gt;修复：斩断回调先于停止播放&lt;/h3&gt;

&lt;p&gt;修复的核心时序非常明确：&lt;strong&gt;在对象死亡之前，必须先切断外部系统对它的访问路径&lt;/strong&gt;。具体到 SDL_mixer，就是先 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_UnregisterAllEffects&lt;/code&gt; 注销 effect 回调，再 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_HaltChannel&lt;/code&gt; 停止播放。&lt;/p&gt;

&lt;p&gt;修改后的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Stop()&lt;/code&gt; 实现：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_CHANNELS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;Mix_UnregisterAllEffects&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;Mix_HaltChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mAutoRelease&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;析构函数现在改为：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::~&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样，当 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 被销毁时，它会主动停止自己绑定的通道，并且在停止之前彻底注销 pitch effect。即使 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_HaltChannel&lt;/code&gt; 的执行存在异步延迟，由于回调已经被取消，混音线程不会再触碰这个垂死对象的任何成员。&lt;/p&gt;

&lt;h3 id=&quot;通道保留与稳健边界&quot;&gt;通道保留与稳健边界&lt;/h3&gt;

&lt;p&gt;除了时序修复，笔者还为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundInstance&lt;/code&gt; 增加了&lt;strong&gt;通道保留机制&lt;/strong&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDLSoundManager&lt;/code&gt; 的空闲通道查找和实例创建逻辑天然确保了每个实例都有一个确定的通道，但旧代码没有把这个信息传递给实例。修复后：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 构造时传入预留的通道号&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSoundManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theSoundManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                   &lt;span class=&quot;n&quot;&gt;Mix_Chunk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theSourceSound&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                   &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theReservedChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// SDLSoundManager 中&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;mPlayingSounds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aFreeChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SDLSoundInstance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSourceSounds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theSfxID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aFreeChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;播放时优先请求该保留通道：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTargetChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mReservedChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mReservedChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_CHANNELS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mReservedChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;mChannel&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Mix_PlayChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aTargetChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mMixChunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;looping&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mix_PlayChannel&lt;/code&gt; 返回 -1（例如该通道被其他高优先级声音抢占），&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mChannel&lt;/code&gt; 会被显式设为 -1，避免残留旧值导致后续操作越界。&lt;/p&gt;

&lt;p&gt;同时，所有涉及 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mChannel&lt;/code&gt; 的边界判断都收紧为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mChannel &amp;gt;= 0 &amp;amp;&amp;amp; mChannel &amp;lt; MAX_CHANNELS&lt;/code&gt;，杜绝负数索引或越界数组访问的风险。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;动画附件系统的崩溃和 SDL 音频回调的崩溃，表面上分属两个毫不相干的子系统，但它们共享同一个底层模式：&lt;strong&gt;手动管理内存的 C++ 对象在析构阶段，外部 C 风格的异步系统仍在通过指针或回调访问它&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;对 Reanimator 的修复思路是&lt;strong&gt;建立显式的所有权清理和垃圾回收&lt;/strong&gt;：确保 Attachment 的 effect 数组与底层粒子/动画/Trail 的实际生命周期同步，并在分配压力下主动回收死对象。对 SDL 音频的修复思路则是&lt;strong&gt;在对象死亡之前，按正确顺序切断外部回调&lt;/strong&gt;：先 UnregisterEffects，再 HaltChannel，析构函数绝不空手而归。&lt;/p&gt;

&lt;p&gt;这两种思路——清理引用链和切断访问路径——是处理 C++ 与底层 C API 混用时生命周期问题的基本工具。PvZ-Portable 作为对 legacy 引擎的重实现，在处理这类历史遗留假设时仍需持续投入精力。&lt;/p&gt;

&lt;h2 id=&quot;️-版权与说明&quot;&gt;⚠️ 版权与说明&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;重要：本项目仅包含代码引擎，不包含任何游戏素材！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PvZ-Portable 严格遵守版权协议。游戏的 IP（植物大战僵尸）属于 PopCap/EA。&lt;/p&gt;

&lt;p&gt;要研究或使用此项目，你&lt;strong&gt;必须&lt;/strong&gt;拥有正版游戏（如果没有，请在 &lt;a href=&quot;https://store.steampowered.com/app/3590/Plants_vs_Zombies_GOTY_Edition/&quot;&gt;Steam&lt;/a&gt; 或 &lt;a href=&quot;https://www.ea.com/games/plants-vs-zombies/plants-vs-zombies&quot;&gt;EA 官网&lt;/a&gt; 上购买）。你需要从正版游戏中提取以下文件放到 PvZ-Portable 的程序所在目录中。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.pak&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties/&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本项目的源代码以 &lt;a href=&quot;https://www.gnu.org/licenses/lgpl-3.0.html&quot;&gt;&lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;&lt;/a&gt; 许可证开源，欢迎学习和贡献。&lt;/p&gt;
</description>
        <pubDate>Fri, 10 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/10/PvZ-Portable-Resource-Lifetime-Safety/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/10/PvZ-Portable-Resource-Lifetime-Safety/</guid>
        
        <category>C++</category>
        
        <category>SDL2</category>
        
        <category>游戏移植</category>
        
        <category>开源软件</category>
        
        <category>开源游戏</category>
        
        <category>PvZ-Portable</category>
        
        
      </item>
    
      <item>
        <title>PvZ-Portable：渲染层缓冲区的安全重构与边界保护</title>
        <description>&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;此前，笔者集中修复了渲染子系统中一类隐藏很深的底层缺陷：OpenGL 顶点缓冲中的越界风险、不匹配的内存释放，以及缺失的边界防护。这些问题与游戏逻辑无关，却在复杂特效或高负载场景下有直接触发崩溃的风险。&lt;/p&gt;

&lt;p&gt;本文将详细记录这次对 PvZ-Portable 渲染层的安全重构过程，包括裸数组的 RAII 化、顶点追加时序的修正、粒子特效裁剪阶段的溢出保护，以及一个长期潜伏的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete/delete[]&lt;/code&gt; 不匹配问题。&lt;/p&gt;

&lt;h2 id=&quot;旧顶点缓冲区的隐患&quot;&gt;旧顶点缓冲区的隐患&lt;/h2&gt;

&lt;p&gt;PvZ-Portable 的 OpenGL 渲染后端采用批量提交（batching）策略：将尽可能多的几何体合并到同一个 draw call 中，再统一调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glDrawArrays&lt;/code&gt;。为了暂存这些顶点数据，引擎维护了一个全局的静态缓冲区 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices&lt;/code&gt;。在重构之前，它的实现非常直接——一块通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new[]&lt;/code&gt; 分配的固定大小裸数组：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 初始化&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MAX_VERTICES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;所有向 GPU 提交顶点的操作都围绕这个数组进行。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxAddVertices&lt;/code&gt; 负责将新的顶点 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memcpy&lt;/code&gt; 到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices + gNumVertices&lt;/code&gt; 的位置，然后累加计数器。旧代码的逻辑看起来大致是这样：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GfxAddVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;memcpy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_VERTICES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;GfxEnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;      &lt;span class=&quot;c1&quot;&gt;// 提交当前批次&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;GfxBegin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 开始新批次&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码的问题在于&lt;strong&gt;边界检查的时序&lt;/strong&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memcpy&lt;/code&gt; 发生在容量判定之前，这意味着如果某一次调用传入的顶点数量直接超过了数组剩余容量，数据会在边界检查触发 flush 之前就写到了数组末尾之外。这是明确的未定义行为，可能导致直接崩溃。&lt;/p&gt;

&lt;p&gt;此外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices&lt;/code&gt; 作为全局裸指针，其生命周期完全依赖 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GLInterface&lt;/code&gt; 的构造函数和析构函数手动管理。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new[]&lt;/code&gt; 与 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete[]&lt;/code&gt; 虽然配对正确，但整个设计没有任何自动边界保护或异常安全保证。&lt;/p&gt;

&lt;h2 id=&quot;向-stdvector-重构与追加前-flush&quot;&gt;向 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 重构与追加前 flush&lt;/h2&gt;

&lt;p&gt;修复的第一步是彻底改造顶点缓冲的内存模型。笔者将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices&lt;/code&gt; 从裸指针替换为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&amp;lt;GLVertex&amp;gt;&lt;/code&gt;，利用 C++ 标准容器的自动扩容和 RAII 语义来消除手动分配风险：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;std&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vector&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果只是简单地把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memcpy&lt;/code&gt; 改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;push_back&lt;/code&gt;，时序问题依然存在。真正的关键是&lt;strong&gt;把边界检查从追加后移到追加前&lt;/strong&gt;。为此，笔者引入了一个独立的辅助函数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxFlushIfOverBudget()&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GfxFlushIfOverBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_VERTICES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;GfxEnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;GfxBegin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个函数在每次追加操作完成后被调用，用于判断当前累积的顶点数是否已经达到了单批次提交的上限。如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gNumVertices&lt;/code&gt; 已经触及 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_VERTICES&lt;/code&gt;，它会立即强制提交当前批次，清空缓冲区，然后开始新的批次。结合 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 的使用，新的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxAddVertices&lt;/code&gt; 变成了下面这样：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GfxAddVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;resize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;memcpy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arrCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;GfxFlushIfOverBudget&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resize(gNumVertices)&lt;/code&gt; 会在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 需要扩容时自动重新分配内存，并安全地迁移已有数据，这意味着 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memcpy&lt;/code&gt; 的目标地址 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices.data() + oldCount&lt;/code&gt; 始终是合法的——无论 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gNumVertices&lt;/code&gt; 增长到多大，都不会再出现裸数组时代的写越界。&lt;/p&gt;

&lt;p&gt;随之而来的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_VERTICES&lt;/code&gt; 的语义转变：在裸数组时代，它是防止缓冲区溢出的硬边界；而在使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 之后，它变成了&lt;strong&gt;单批次向 GPU 提交顶点的上限&lt;/strong&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxFlushIfOverBudget&lt;/code&gt; 保障的是渲染性能与驱动兼容性，而不是防止内存损坏。也就是说，即使某次追加后 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gNumVertices&lt;/code&gt; 暂时超过了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_VERTICES&lt;/code&gt;，数据本身仍然是安全地存放在已扩容的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 中的，唯一的后果是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxEnd()&lt;/code&gt; 被触发时，这次向 GPU 提交的 draw call 可能会略微大于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_VERTICES&lt;/code&gt; 的设定值。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GfxEnd()&lt;/code&gt; 本身也得到了加固：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;GfxEnd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;glBindBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GL_ARRAY_BUFFER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gVbo&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;glBufferData&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GL_ARRAY_BUFFER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                     &lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GL_DYNAMIC_DRAW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// 设置 vertex attributes ...&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;glDrawArrays&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;gVertexMode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GLenum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gNumVertices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gVertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;绘制命令现在只在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gNumVertices &amp;gt; 0&lt;/code&gt; 时才执行，避免了对空缓冲区发起无意义的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glDrawArrays&lt;/code&gt;。批次结束后 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gVertices.clear()&lt;/code&gt; 会保留已分配的容量（因为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clear()&lt;/code&gt; 不释放内存），这样下一帧的追加通常不需要重新进行堆分配。&lt;/p&gt;

&lt;p&gt;在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GLInterface&lt;/code&gt; 的析构函数中，旧代码需要显式 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete[] gVertices&lt;/code&gt;，现在这一步也被 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;std::vector&lt;/code&gt; 的自动析构所取代，彻底消除了手动内存管理中的泄漏风险。&lt;/p&gt;

&lt;h2 id=&quot;裁剪阶段的溢出保护&quot;&gt;裁剪阶段的溢出保护&lt;/h2&gt;

&lt;p&gt;顶点缓冲区的主路径安全了，但还有一个侧路径同样危险——&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EffectSystem&lt;/code&gt; 中的三角形裁剪与追加逻辑。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TodTriangleGroup::AddTriangle&lt;/code&gt; 负责接收一个三角形，根据当前的裁剪矩形将其切分为多个子三角形，然后把结果写入 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mVertArray&lt;/code&gt; 数组。这个数组的大小由 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_TRIANGLES&lt;/code&gt; 定义。旧代码在循环内直接写入：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Tod_clipShape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clipped&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aTriRef&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clipX0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clipX1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clipY0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clipY1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;TriVertex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pVert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mVertArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mTriangleCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pVert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clipped&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 填充顶点数据 ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mTriangleCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;问题在于，当一个复杂的粒子特效触发裁剪时，单个输入三角形可能被切分成数十个子三角形。如果此时 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mTriangleCount&lt;/code&gt; 已经接近 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MAX_TRIANGLES&lt;/code&gt;，循环内没有任何容量检查，最终一定会写穿 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mVertArray&lt;/code&gt; 的边界。&lt;/p&gt;

&lt;p&gt;这一问题只有在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mVertArray&lt;/code&gt; 接近其容量上限时才会显现，因此之前长期未被发现。修复非常直接：在每次增量前检查容量，若已满则先 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DrawGroup(g)&lt;/code&gt; 提交当前积攒的三角形：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;j&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mTriangleCount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MAX_TRIANGLES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;DrawGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;TriVertex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pVert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mVertArray&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mTriangleCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// ... 填充顶点数据 ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mTriangleCount&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;由于这种极端裁剪场景触发概率不高，再加上不同平台内存分配器对越界写入的容忍度不同，这个缺陷在此前并未引起足够注意。但在更严格的运行环境（如特定的 GPU 驱动）下，以及某些无尽模式情形的高压力下，能稳定复现崩溃。&lt;/p&gt;

&lt;h2 id=&quot;隐藏已久的-deletedelete-不匹配&quot;&gt;隐藏已久的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete[]&lt;/code&gt; 不匹配&lt;/h2&gt;

&lt;p&gt;在审查顶点缓冲相关代码时，笔者还注意到了一个与 OpenGL 无关但同样危险的内存管理错误，它藏在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VertexList&lt;/code&gt; 结构体中。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VertexList&lt;/code&gt; 是一个小型的动态数组实现，用于临时存放一组 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GLVertex&lt;/code&gt;。它的内部分配逻辑是这样的：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;reserve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theCapacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCapacity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mCapacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNewList&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GLVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theCapacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;memcpy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aNewList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mSize&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]));&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mStackVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aNewList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;以及析构函数：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;VertexList&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mStackVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mVerts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里的问题非常经典：&lt;strong&gt;使用了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new[]&lt;/code&gt; 分配数组，却用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt;（标量删除）来释放&lt;/strong&gt;。根据 C++ 标准，这是未定义行为。虽然标准的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete[]&lt;/code&gt; 在简单类型如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GLVertex&lt;/code&gt; 上可能不会立刻崩溃，但它们在底层可能调用不同的释放逻辑：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new[]&lt;/code&gt; 通常会在返回的指针前面额外存储数组元素个数；&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete&lt;/code&gt; 不会读取这个前缀，只会释放单个对象大小的内存；&lt;/li&gt;
  &lt;li&gt;这会导致分配的内存和释放的内存大小不一致，逐渐腐蚀堆的元数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在短生命周期对象上，这个错误可能数年都不被察觉。但当引擎运行时间变长、对象分配变得非常频繁时，堆元数据的损坏会在完全无关的代码位置引发难以解释的崩溃。修复只需要把两处 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete mVerts;&lt;/code&gt; 改成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delete[] mVerts;&lt;/code&gt;。&lt;strong&gt;未定义行为不会因为当前平台暂未崩溃而变得安全&lt;/strong&gt;。不同的内存分配器实现对这种不匹配的敏感程度各不相同，在一种环境下稳定的程序，换到另一种环境下就可能出现难以追踪的崩溃。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;这次对渲染层缓冲区安全的集中修复，本质上是对底层代码中遗漏边界和生命周期管理的系统补齐。这些缺陷在过去可能因为触发概率低、触发场景少而没有立刻暴露，但它们始终是潜在的风险点。&lt;/p&gt;

&lt;p&gt;PvZ-Portable 的跨平台工作还在持续，这类底层基础设施的健壮化也将是后续的重点方向。&lt;/p&gt;

&lt;h2 id=&quot;️-版权与说明&quot;&gt;⚠️ 版权与说明&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;重要：本项目仅包含代码引擎，不包含任何游戏素材！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PvZ-Portable 严格遵守版权协议。游戏的 IP（植物大战僵尸）属于 PopCap/EA。&lt;/p&gt;

&lt;p&gt;要研究或使用此项目，你&lt;strong&gt;必须&lt;/strong&gt;拥有正版游戏（如果没有，请在 &lt;a href=&quot;https://store.steampowered.com/app/3590/Plants_vs_Zombies_GOTY_Edition/&quot;&gt;Steam&lt;/a&gt; 或 &lt;a href=&quot;https://www.ea.com/games/plants-vs-zombies/plants-vs-zombies&quot;&gt;EA 官网&lt;/a&gt; 上购买）。你需要从正版游戏中提取以下文件放到 PvZ-Portable 的程序所在目录中。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.pak&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties/&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本项目的源代码以 &lt;a href=&quot;https://www.gnu.org/licenses/lgpl-3.0.html&quot;&gt;&lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;&lt;/a&gt; 许可证开源，欢迎学习和贡献。&lt;/p&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/09/PvZ-Portable-GL-Buffer-Safety/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/09/PvZ-Portable-GL-Buffer-Safety/</guid>
        
        <category>C++</category>
        
        <category>OpenGL</category>
        
        <category>SDL2</category>
        
        <category>游戏移植</category>
        
        <category>开源软件</category>
        
        <category>开源游戏</category>
        
        <category>PvZ-Portable</category>
        
        
      </item>
    
      <item>
        <title>Qt Web Extractor 新增 MCP 支持：为 AI 编程助手提供高级网页提取</title>
        <description>&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;

&lt;p&gt;之前笔者写过一篇介绍 &lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 的文章，讲了这个项目是怎么用系统 Qt WebEngine 来做轻量级网页内容提取的。项目一直提供了 HTTP REST API 和 Open WebUI 的集成方式，给 AI 平台调用本来就不是问题——在 Open WebUI 里配置一下外部网页加载器，或者直接用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tool.py&lt;/code&gt; 作为自定义工具，都能很方便地让 LLM 获取网页内容，几乎是一键式集成。&lt;/p&gt;

&lt;p&gt;不过对于 Claude Code、OpenCode 这类 AI 编程助手，&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 此前事实上还不能开箱即用，需要手动配置。这些工具本身支持 MCP 协议来扩展能力，如果网页提取服务也能以 MCP 的方式暴露出来，那在终端对话中需要查阅在线文档、阅读 API 参考的时候，AI 助手就能直接调用，不需要任何额外的脚本或配置。&lt;/p&gt;

&lt;p&gt;与简单的 HTTP 抓取服务不同，本工具有能力处理目前占据主流的复杂动态渲染页面，不仅能&lt;strong&gt;提取出其他基础工具获取不到的动态内容&lt;/strong&gt;，基于完整渲染页面转换得到的 Markdown 还保留了&lt;strong&gt;超链接、表格结构、代码块&lt;/strong&gt;等丰富结构信息。这意味着模型在阅读内容后，可以顺着超链接继续获取相关的参考文档，形成信息获取的良性循环。&lt;/p&gt;

&lt;p&gt;所以这次更新，项目在原有的 HTTP 服务基础上内建了 &lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP（Model Context Protocol）&lt;/a&gt; 支持。这算是锦上添花——几乎没有引入任何额外的代码负担和运行开销，只是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt; 里多处理了一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mcp&lt;/code&gt; 端点，但大大方便了终端 AI 工具的使用体验。Claude Code、OpenCode 这类工具在对话中遇到需要读取网页内容的场景时，会调用这个 MCP 工具，整个过程非常顺畅。&lt;/p&gt;

&lt;h2 id=&quot;什么是-mcp&quot;&gt;什么是 MCP&lt;/h2&gt;

&lt;p&gt;MCP 是 Anthropic 提出的一种开放协议，用来标准化 AI 模型与外部工具之间的交互方式。服务端按照协议暴露一组工具（tools），客户端通过标准的 JSON-RPC 调用来请求执行，服务端返回结构化的结果。&lt;/p&gt;

&lt;p&gt;对于 Qt Web Extractor 来说，MCP 的价值不在于让 AI 能调用——这一点原来的 REST API 和 Open WebUI 集成已经做得很好了——而在于让 Claude Code、OpenCode 这类终端 AI 工具能&lt;strong&gt;原生地发现和使用&lt;/strong&gt;这个能力。AI 助手启动时会读取可用的 MCP 工具列表，在对话中遇到需要读取网页的场景时，会自动选择调用，整个过程不需要用户手动干预。&lt;/p&gt;

&lt;p&gt;另外，笔者在工具描述中特别写了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Always prioritize this tool when you need to read, fetch, or analyze content from any URL or web link&lt;/code&gt;，这样 AI 模型在多个可用工具中会&lt;strong&gt;优先选择本工具&lt;/strong&gt;来处理网页内容，避免使用自带的简单 HTTP 请求工具导致无法获取动态渲染内容或者格式不佳的情况。&lt;/p&gt;

&lt;h2 id=&quot;实现方式&quot;&gt;实现方式&lt;/h2&gt;

&lt;p&gt;Qt Web Extractor 的 MCP 支持是直接内建在已有的 HTTP 服务里的，不需要额外启动任何进程。服务启动后，除了原有的 REST API 端点，还会在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/mcp&lt;/code&gt; 路径上响应 MCP 协议的 JSON-RPC 请求。&lt;/p&gt;

&lt;h3 id=&quot;协议处理&quot;&gt;协议处理&lt;/h3&gt;

&lt;p&gt;MCP 基于 JSON-RPC 2.0，服务端需要处理几个标准方法：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initialize&lt;/code&gt;：客户端初始化握手，返回协议版本和服务端信息&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ping&lt;/code&gt;：心跳检测&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tools/list&lt;/code&gt;：返回可用的工具列表&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tools/call&lt;/code&gt;：执行具体的工具调用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些都在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_Handler&lt;/code&gt; 类中实现。以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tools/list&lt;/code&gt; 为例，它会返回一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_url&lt;/code&gt; 工具的定义：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;fetch_url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;Advanced web content extractor. Fully evaluates JavaScript &quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;to render modern web pages and converts the result into clean &quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;Markdown. Always prioritize this tool when you need to read, &quot;&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;fetch, or analyze content from any URL or web link.&quot;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;inputSchema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;The URL to extract content from.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;additionalProperties&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;_meta&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&quot;anthropic/maxResultSizeChars&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;500000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;工具描述里明确告诉 AI 模型这是一个高级网页提取器，能完整执行 JavaScript 并返回干净的 Markdown 格式内容。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_meta&lt;/code&gt; 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;anthropic/maxResultSizeChars&lt;/code&gt; 是 Anthropic 的扩展字段，用来告知客户端单次返回的最大字符数。&lt;/p&gt;

&lt;h3 id=&quot;提取流程&quot;&gt;提取流程&lt;/h3&gt;

&lt;p&gt;当 AI 助手调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_url&lt;/code&gt; 工具时，服务端会走和 REST API 相同的提取管线：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;解析请求参数，获取目标 URL&lt;/li&gt;
  &lt;li&gt;自动检测是否为 PDF 链接（先检查 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.pdf&lt;/code&gt; 后缀作为快速路径，否则发送 HEAD 请求检查 Content-Type）&lt;/li&gt;
  &lt;li&gt;将提取请求放入队列，由 Qt 主线程的 WebEngine 执行渲染&lt;/li&gt;
  &lt;li&gt;等待页面加载完成，提取 Markdown 文本&lt;/li&gt;
  &lt;li&gt;返回结构化结果，包含 URL、标题、Markdown 内容和可能的错误信息&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;返回格式遵循 MCP 的工具调用规范：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;页面 Markdown 内容...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;structuredContent&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;title&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Example Domain&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;markdown&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;页面 Markdown 内容...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;isError&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content&lt;/code&gt; 字段是 AI 模型直接阅读的文本，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;structuredContent&lt;/code&gt; 则保留了完整的结构化数据，方便客户端做进一步处理。&lt;/p&gt;

&lt;h3 id=&quot;架构设计&quot;&gt;架构设计&lt;/h3&gt;

&lt;p&gt;Qt WebEngine 的页面加载必须在 Qt 主线程的事件循环中运行，而 HTTP 服务是在后台线程处理的。两者之间的通信通过一个线程安全的队列来实现：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;_ExtractRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;__slots__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;pdf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;result&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;done&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pdf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;url&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pdf&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pdf&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_ExtractionResult&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;done&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;threading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;HTTP 处理线程创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_ExtractRequest&lt;/code&gt; 放入队列，Qt 主线程从队列中取出请求、执行提取、设置结果并触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;done&lt;/code&gt; 事件。HTTP 线程等待 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;done&lt;/code&gt; 信号后读取结果返回给客户端。这个设计和原有的 REST API 共享同一套机制，MCP 只是多了一个协议解析层。&lt;/p&gt;

&lt;h2 id=&quot;使用方式&quot;&gt;使用方式&lt;/h2&gt;

&lt;h3 id=&quot;启动服务&quot;&gt;启动服务&lt;/h3&gt;

&lt;p&gt;首先启动 Qt Web Extractor 的 HTTP 服务：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;qt-web-extractor serve &lt;span class=&quot;nt&quot;&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;nt&quot;&gt;--port&lt;/span&gt; 8766
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果需要 API Key 认证：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;qt-web-extractor serve &lt;span class=&quot;nt&quot;&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;nt&quot;&gt;--port&lt;/span&gt; 8766 &lt;span class=&quot;nt&quot;&gt;--api-key&lt;/span&gt; mysecretkey
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;claude-code&quot;&gt;Claude Code&lt;/h3&gt;

&lt;p&gt;Claude Code 提供了命令行方式来添加 MCP 服务端：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 无认证&lt;/span&gt;
claude mcp add &lt;span class=&quot;nt&quot;&gt;--transport&lt;/span&gt; http web-extractor http://127.0.0.1:8766/mcp

&lt;span class=&quot;c&quot;&gt;# 如果服务端设置了 --api-key&lt;/span&gt;
claude mcp add &lt;span class=&quot;nt&quot;&gt;--transport&lt;/span&gt; http web-extractor http://127.0.0.1:8766/mcp &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--header&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer mysecretkey&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;也可以通过配置文件来管理。项目根目录的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mcp.json&lt;/code&gt; 作用于当前项目，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.claude.json&lt;/code&gt; 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mcpServers&lt;/code&gt; 则是全局配置：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcpServers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;web-extractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1:8766/mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;headers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bearer mysecretkey&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;opencode&quot;&gt;OpenCode&lt;/h3&gt;

&lt;p&gt;OpenCode 的配置写在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opencode.json&lt;/code&gt;（项目根目录）或 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.config/opencode/opencode.json&lt;/code&gt;（全局）中：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://opencode.ai/config.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;web_extractor&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;remote&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;http://127.0.0.1:8766/mcp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;enabled&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;headers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Authorization&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bearer mysecretkey&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;关于认证&quot;&gt;关于认证&lt;/h3&gt;

&lt;p&gt;MCP 端点和 REST API 共用同一个认证机制。手动启动服务时通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--api-key&lt;/code&gt; 设置密钥，而使用 systemd 服务时则是在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/qt-web-extractor.conf&lt;/code&gt; 中取消 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;API_KEY=&lt;/code&gt; 的注释并填入密钥。无论哪种方式，客户端在请求头中带上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code&gt; 即可通过验证。如果不设置 API Key，所有端点都无需认证即可访问。&lt;/p&gt;

&lt;h3 id=&quot;实际使用场景&quot;&gt;实际使用场景&lt;/h3&gt;

&lt;p&gt;配置好以后，用起来就很自然了。比如在 OpenCode 里问一句：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; 帮我看看 https://github.com/wszqkzqk/qt-web-extractor 是怎么设计的
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;模型会自动调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch_url&lt;/code&gt; 工具把页面内容拉下来，然后基于渲染后的完整页面转化得到的干净整洁的 Markdown 来回答。对于查阅在线文档、对比 API 设计、看某个项目的使用说明这些场景，这个流程都颇为顺畅。&lt;/p&gt;

&lt;h2 id=&quot;与原有功能的配合&quot;&gt;与原有功能的配合&lt;/h2&gt;

&lt;p&gt;MCP 支持只是多了一个接口，不影响原有的任何使用方式。项目目前提供了四种使用方式：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;命令行工具&lt;/strong&gt;：快速提取，适合脚本和一次性使用&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Python API&lt;/strong&gt;：作为库集成到自己的项目中&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTP REST API&lt;/strong&gt;：为 Open WebUI 等平台提供网页加载服务&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MCP 协议&lt;/strong&gt;：为 Claude Code、OpenCode 等终端 AI 工具提供原生调用&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;它们共享同一个提取引擎，底层都是 Qt WebEngine 在执行渲染。后台服务可以同时响应 REST API 和 MCP 请求，互不干扰。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;这次 MCP 支持的加入，对于日常使用 Claude Code、OpenCode 等终端 AI 助手的开发者来说，查阅在线文档、分析网页内容已经变得和读写本地文件一样自然。&lt;/p&gt;

&lt;p&gt;项目依然保持了轻量、简洁的特点——没有引入任何额外的依赖，MCP 协议处理只是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.py&lt;/code&gt; 中新增的几百行代码，几乎不存在额外开销。服务启动后，REST API 和 MCP 端点同时可用，按需取用。&lt;/p&gt;

&lt;p&gt;项目仓库地址：&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;GitHub · Qt Web Extractor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;项目协议：&lt;a href=&quot;https://www.gnu.org/licenses/gpl-3.0.html&quot;&gt;GPL-3.0-or-later&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/04/qt-web-extractor-mcp/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/04/qt-web-extractor-mcp/</guid>
        
        <category>Python</category>
        
        <category>Qt</category>
        
        <category>PySide</category>
        
        <category>开源软件</category>
        
        <category>LLM</category>
        
        <category>MCP</category>
        
        
      </item>
    
      <item>
        <title>Arch Linux for Loong64 构建与调试的 AI 辅助 SKILL 编写记</title>
        <description>&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;

&lt;p&gt;近年我们在为 Arch Linux 适配 LoongArch 架构（loong64）的过程中，遇到了大量软件包构建、移植和调试的挑战。Arch Linux 的官方打包体系底层依赖 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devtools&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;archbuild&lt;/code&gt; 系列工具箱。虽然这套构建工具非常强大，利用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd-nspawn&lt;/code&gt; 容器和 Btrfs 快照实现了极高的环境隔离性，让宿主机保持干净整洁，但它的复杂性也带来了潜在的门槛。&lt;/p&gt;

&lt;p&gt;随着 AI 辅助编程工具日益普及，笔者在日常的打包维护工作中，越来越多地尝试让大模型参与解决某些构建失败排查、修复补丁并处理依赖升档的任务。然而，通用性很强的大模型一旦脱离了传统应用层代码，进入到 Arch Linux 那错综复杂的隔离构建黑盒中，常常会暴露出极大的幻觉：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;常常试图在宿主机的 Git 仓库里直接调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;make&lt;/code&gt; 而非在 chroot 内部操作。&lt;/li&gt;
  &lt;li&gt;搞不清楚挂载目录的权限（例如盲目尝试去修改被只读挂载的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/startdir/PKGBUILD&lt;/code&gt;）。&lt;/li&gt;
  &lt;li&gt;完全没有“保护干净模板”的潜意识，甚至有时会直接污染存放 root 模版的只读系统目录。&lt;/li&gt;
  &lt;li&gt;在需要连续构建多个相互依赖的包（例如 soname bump 的连锁重构）时，根本不知道要如何搭建并复用本地的 pacman 软件源。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为此，笔者专门编写了一套针对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;archlinux-loong64-portbuild&lt;/code&gt; 工作流的专属 SKILL 文件。本文主要讨论这份 SKILL 的技术设计思路、覆盖的使用场景，并探讨如何通过提供高质量的上下文指导系统，把通用大语言模型真正调优成一位“Arch Linux 官方打包团队专家”。&lt;/p&gt;

&lt;h2 id=&quot;核心痛点与系统设计理念&quot;&gt;核心痛点与系统设计理念&lt;/h2&gt;

&lt;p&gt;由于模型本身并不是一个真实的 Linux 用户，我们要赋予它解决问题的能力，就必须先把 Archbuild 底层的运转流程灌输给它。在整个 SKILL 文件中，笔者着重强调了以下几条核心的设计理念。&lt;/p&gt;

&lt;h3 id=&quot;环境隔离&quot;&gt;环境隔离&lt;/h3&gt;

&lt;p&gt;这是阻挡模型瞎乱操作的第一道防线。笔者在设定上，将“宿主机”、“Btrfs 快照工作区”以及“干净的 Chroot 模板”做了非常清晰的职能划分。模型被要求严格遵守“宿主机禁止直接编译”和“绝不污染模板环境”两条铁律。&lt;/p&gt;

&lt;p&gt;为了彻底解决 AI 读错文件目录的问题，SKILL 中抽象定义了一张&lt;strong&gt;路径映射表（Path Mapping）&lt;/strong&gt;。一旦构建失败，模型需要查阅日志并自动将宿主机的仓库路径转换到容器内的工作路径进行思考。例如，向模型讲清楚 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sed&lt;/code&gt; 的目标应该位于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/build/&amp;lt;pkgbase&amp;gt;/src/&lt;/code&gt;，而不是宿主机所在的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/startdir/&lt;/code&gt;。这不仅避免了误操作，也使 AI 分配的调试命令不再充满错误路径。&lt;/p&gt;

&lt;h3 id=&quot;规范调试与固化工作流&quot;&gt;规范调试与固化工作流&lt;/h3&gt;

&lt;p&gt;没有约束的 AI 会像盲头苍蝇。面对编译报错（如段错误或链接错误），模型最喜欢做的事情就是随意修改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PKGBUILD&lt;/code&gt; 然后建议用户重头再跑一遍 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;extra-loong64-build&lt;/code&gt;，导致每次都要重新下载数十兆的源码跑好几分钟——这极大浪费了生命和算力。&lt;/p&gt;

&lt;p&gt;因此，笔者在 SKILL 中要求其遵循一种符合人类专家习惯的标准工作流：&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;保存现场&lt;/strong&gt;：建议用户使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;btrfs subvolume snapshot&lt;/code&gt; 将出错容器备份。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;切入环境&lt;/strong&gt;：利用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd-nspawn -aD&lt;/code&gt; 配合正确的用户身份进入已有容器。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;定位分析&lt;/strong&gt;：进入真正的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/build/&lt;/code&gt; 目录进行快速迭代（只需局部使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;makepkg --check&lt;/code&gt; 等轻量化指令即可快速验证，避免全量重建）。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;把补丁冲突降至最低&quot;&gt;把补丁冲突降至最低&lt;/h3&gt;

&lt;p&gt;在进行 loong64 的特殊移植时，最让人头疼的莫过于我们针对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PKGBUILD&lt;/code&gt; 打的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loong.patch&lt;/code&gt;，在上游原仓库一有常规小更新时就会引发大面积的 Git merge 冲突。这也常常是让 AI 帮忙更新补丁时的问题——通用 AI 给出的补丁修复策略往往是“替换原数组某一行”，非常生硬。&lt;/p&gt;

&lt;p&gt;为此笔者在 SKILL 定义了明确地修改安全范式：&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;永远优先追加（Append）&lt;/strong&gt;：对于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;makedepends&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;depends&lt;/code&gt; 或者是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; 哈希数组，必须使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+=()&lt;/code&gt; 在文件尾部追加，即使上游把核心数组的顺序全改了也不受任何影响。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;动态数组过滤&lt;/strong&gt;：当碰到必须删除某些架构不支持的组件依赖（例如在国产平台上经常不需要的 cuda 组件时），不再直接去原生的声明列表里删词，而是建议通过 Bash 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grep -Ev&lt;/code&gt; 对上游赋值完的数组进行尾部动态过滤。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;代码段多行注释&lt;/strong&gt;：不要删除无法使用的全段编译代码，而是用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;: &amp;lt;&amp;lt;COMMENT ... COMMENT&lt;/code&gt; 将其完整包裹。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这可以使 AI 生成的移植补丁可以更加健壮。&lt;/p&gt;

&lt;h3 id=&quot;为大规模依赖重构护航local-repo-自举链&quot;&gt;为大规模依赖重构护航：Local Repo 自举链&lt;/h3&gt;

&lt;p&gt;当我们处理核心共享库升级带来的连带反应时，必须引入本地仓库作为引导桥梁。为此笔者在 SKILL 文件的进阶部分，完整传授了&lt;strong&gt;临时本地仓库的使用流程&lt;/strong&gt;。告知 AI 如何借助 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repo-add&lt;/code&gt; 工具以及编写带有优先级的 pacman 配置，从而引导其顺利通过诸如“包A -&amp;gt; 需依赖刚打出的包B -&amp;gt; 再编译新的包C” 这种 Bootstrap 的死锁链路，这一过程彻底解放了笔者的双手。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;通过将深度的领域知识转化为结构化的 SKILL 资产后，我惊讶地发现 AI 协助我们推进各种偏底层的系统组件适配效率实现了跨越式提升。原本排查 QEMU User 模式与真机引发各种诡异的 Segment fault 时只能抓瞎，但在有了完善的检查清单和路径指引下，它能够在正确的路径调用正确的工具做快速迭代。&lt;/p&gt;

&lt;p&gt;只有基于严谨定义的专家心智模型，智能工具才能真正切中技术细节的痛点。这份 SKILL 文件不仅是对我们阶段性踩坑方案的记录，其实也是一部留给新人开发者的速成参考。未来，随着 LoongArch 整个新世界软生态的不断拓展，类似这样沉淀下来的系统化工程经验将持续发挥它们的指路作用。&lt;/p&gt;

&lt;h2 id=&quot;附录skill-内容完整再现&quot;&gt;附录：SKILL 内容完整再现&lt;/h2&gt;

&lt;p&gt;这套机制的完整规则可以直接在此阅读。你可以将它挂载到你自己的 AI Agent 或者 IDE 的环境设置中：&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;archlinux-loong64-portbuild&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Assist&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Arch&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Linux&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Loong64&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;(loongarch64)&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;package&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;porting,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;building,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;and&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;debugging&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;using&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;archbuild&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;tools.&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Use&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;when:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;porting&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;loong64,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;debugging&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;archbuild&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;or&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;devtools-loong64&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;failures,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;fixing&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;soname&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;changes,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;etc.,&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;or&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;patching&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;PKGBUILDs.&apos;&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;gh&quot;&gt;# Arch Linux for Loong64 构建与调试指南&lt;/span&gt;

你是一个精通 Arch Linux 软件包构建系统（特别是 &lt;span class=&quot;sb&quot;&gt;`devtools`&lt;/span&gt; 和 &lt;span class=&quot;sb&quot;&gt;`archbuild`&lt;/span&gt;）以及 LoongArch64 架构特性的专家。你的任务是协助用户理解构建流程、定位构建失败原因，并提供安全的调试方案和最适合的修复方案。

&lt;span class=&quot;gu&quot;&gt;## 核心原则：环境隔离 (Environment Isolation)&lt;/span&gt;

&lt;span class=&quot;gs&quot;&gt;**这是最重要的规则：**&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;1.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**宿主机是干净的**&lt;/span&gt;：&lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 所在的 Git 仓库目录&lt;span class=&quot;gs&quot;&gt;**不包含**&lt;/span&gt;构建源码，也不应被用于直接编译或配置环境。
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**Chroot 是唯一的真相来源**&lt;/span&gt;：所有的源码下载、解压、编译、测试都发生在一个隔离的 systemd-nspawn 容器中。
&lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**绝不污染模板**&lt;/span&gt;：&lt;span class=&quot;sb&quot;&gt;`/var/lib/archbuild/.../root`&lt;/span&gt; 是干净的模板环境，&lt;span class=&quot;gs&quot;&gt;**绝对禁止**&lt;/span&gt;进入或修改它。如果它被污染，必须使用 &lt;span class=&quot;sb&quot;&gt;`-c`&lt;/span&gt; 参数重建。
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 1. Archbuild 构建流程详解 (Mental Model)&lt;/span&gt;

当用户运行构建命令（如 &lt;span class=&quot;sb&quot;&gt;`extra-loong64-build`&lt;/span&gt;）时，后台发生了以下流程。理解此流程是调试的关键：
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**环境准备**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   系统读取干净的 chroot 模板（通常位于 &lt;span class=&quot;sb&quot;&gt;`/var/lib/archbuild/extra-loong64-build/root`&lt;/span&gt;）。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   系统创建一个 Btrfs 快照或副本作为用户的工作区（位于 &lt;span class=&quot;sb&quot;&gt;`/var/lib/archbuild/extra-loong64-build/&amp;lt;user-name&amp;gt;`&lt;/span&gt;）。
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**目录挂载**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   宿主机的 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 所在目录（即 &lt;span class=&quot;sb&quot;&gt;`pkgbase`&lt;/span&gt; 目录）被&lt;span class=&quot;gs&quot;&gt;**只读挂载**&lt;/span&gt;到容器内的 &lt;span class=&quot;sb&quot;&gt;`/startdir`&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   这意味着容器内可以看到 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 和 &lt;span class=&quot;sb&quot;&gt;`loong.patch`&lt;/span&gt;，但无法修改它们。
&lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**源码获取与构建**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   容器内的 &lt;span class=&quot;sb&quot;&gt;`makepkg`&lt;/span&gt; 逻辑开始运行（以 &lt;span class=&quot;sb&quot;&gt;`builduser`&lt;/span&gt; 用户身份）。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   源码被下载到 &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/src/`&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   构建过程在 &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt; 中进行。
&lt;span class=&quot;p&quot;&gt;4.&lt;/span&gt;  &lt;span class=&quot;gs&quot;&gt;**清理与卸载**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   构建结束后，&lt;span class=&quot;sb&quot;&gt;`/startdir`&lt;/span&gt; 被卸载。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   如果构建成功，产物会被移回宿主机。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   用户工作区（&lt;span class=&quot;sb&quot;&gt;`&amp;lt;user-name&amp;gt;`&lt;/span&gt;）通常会被保留，直到下次构建同仓库包时被覆盖或清理。
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 2. 关键路径映射表 (Path Mapping)&lt;/span&gt;

在指导用户时，请严格区分&lt;span class=&quot;gs&quot;&gt;**宿主机路径**&lt;/span&gt;和&lt;span class=&quot;gs&quot;&gt;**容器内路径**&lt;/span&gt;：

| 资源 | 宿主机路径 (Host) | 容器内路径 (Inside Chroot) | 备注 |
| :--- | :--- | :--- | :--- |
| &lt;span class=&quot;gs&quot;&gt;**PKGBUILD 仓库**&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`/path/to/pkgbase/`&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`/startdir/`&lt;/span&gt; | 只读挂载，构建结束后卸载 |
| &lt;span class=&quot;gs&quot;&gt;**构建工作区**&lt;/span&gt; | N/A | &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`builduser`&lt;/span&gt; 的家目录 |
| &lt;span class=&quot;gs&quot;&gt;**源码目录**&lt;/span&gt; | N/A | &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/src/`&lt;/span&gt; | 源码实际所在位置 |
| &lt;span class=&quot;gs&quot;&gt;**构建产物**&lt;/span&gt; | N/A | &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/pkg/`&lt;/span&gt; | 打包后的文件 |
| &lt;span class=&quot;gs&quot;&gt;**用户 Chroot**&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`/var/lib/archbuild/extra-loong64-build/&amp;lt;user&amp;gt;/`&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`/`&lt;/span&gt; (根目录) | 完整的文件系统 |
| &lt;span class=&quot;gs&quot;&gt;**干净模板**&lt;/span&gt; | &lt;span class=&quot;sb&quot;&gt;`/var/lib/archbuild/extra-loong64-build/root/`&lt;/span&gt; | N/A | &lt;span class=&quot;gs&quot;&gt;**禁止触碰**&lt;/span&gt; |
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 3. 标准调试工作流 (Debugging Workflow)&lt;/span&gt;

当构建失败，用户需要分析原因时，请引导用户遵循以下步骤，而不是在宿主机盲目猜测：

&lt;span class=&quot;gu&quot;&gt;### 步骤 A：保存现场&lt;/span&gt;
默认情况下，下次构建可能会覆盖当前环境。建议用户先保存快照：
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;btrfs subvolume snapshot /var/lib/archbuild/extra-loong64-build/&amp;lt;user-name&amp;gt; /path/to/snapshot
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 步骤 B：进入环境&lt;/span&gt;
使用 &lt;span class=&quot;sb&quot;&gt;`systemd-nspawn`&lt;/span&gt; 进入出错的容器：
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemd-nspawn &lt;span class=&quot;nt&quot;&gt;-aD&lt;/span&gt; /path/to/snapshot
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;ge&quot;&gt;*注意：进入后默认是 root，需切换到构建用户：`sudo -u builduser bash`*&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 步骤 C：定位与分析&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;1.&lt;/span&gt;  进入构建目录：&lt;span class=&quot;sb&quot;&gt;`cd /build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt;  查看源码：&lt;span class=&quot;sb&quot;&gt;`ls src/`&lt;/span&gt;，检查 &lt;span class=&quot;sb&quot;&gt;`src/`&lt;/span&gt; 下的文件结构。
&lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt;  查看日志：检查 &lt;span class=&quot;sb&quot;&gt;`build-log-all.log`&lt;/span&gt; （该文件不在容器内而是在 PKGBUILD 所在的宿主机目录下）或终端输出，确定失败发生在 &lt;span class=&quot;sb&quot;&gt;`prepare()`&lt;/span&gt;, &lt;span class=&quot;sb&quot;&gt;`build()`&lt;/span&gt;, 还是 &lt;span class=&quot;sb&quot;&gt;`check()`&lt;/span&gt;。

&lt;span class=&quot;gu&quot;&gt;### 步骤 D：修改与重测 (高级)&lt;/span&gt;
如果用户需要修改代码并重新测试（例如修改 &lt;span class=&quot;sb&quot;&gt;`check()`&lt;/span&gt; 逻辑），由于 &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt; 下没有 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt;，需要：
&lt;span class=&quot;p&quot;&gt;1.&lt;/span&gt;  将宿主机的 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 复制到容器内：&lt;span class=&quot;sb&quot;&gt;`cp /startdir/PKGBUILD /build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt; (如果 &lt;span class=&quot;sb&quot;&gt;`/startdir`&lt;/span&gt; 还在) 或从外部复制。
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt;  在 &lt;span class=&quot;sb&quot;&gt;`/build/&amp;lt;pkgbase&amp;gt;/`&lt;/span&gt; 下运行：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   仅重跑检查：&lt;span class=&quot;sb&quot;&gt;`makepkg --check`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   重新打包：&lt;span class=&quot;sb&quot;&gt;`makepkg -R`&lt;/span&gt; (产物在 &lt;span class=&quot;sb&quot;&gt;`/srcpkgdest/`&lt;/span&gt;)
&lt;span class=&quot;ge&quot;&gt;*警告：此方法仅用于本地调试，生成的包严禁直接上传。*&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 步骤 E：处理 Git 仓库 (离开环境后)&lt;/span&gt;
&lt;span class=&quot;sb&quot;&gt;`archbuild`&lt;/span&gt; 卸载 &lt;span class=&quot;sb&quot;&gt;`/startdir`&lt;/span&gt; 后，容器内的 git 仓库会因为找不到 object 而报错。
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**方案 1 (推荐)**&lt;/span&gt;：在启动 &lt;span class=&quot;sb&quot;&gt;`systemd-nspawn`&lt;/span&gt; 时重新绑定挂载：
    &lt;span class=&quot;sb&quot;&gt;`sudo systemd-nspawn -aD ... --bind /path/to/host/pkgbase:/startdir`&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**方案 2**&lt;/span&gt;：修改 &lt;span class=&quot;sb&quot;&gt;`.git/objects/info/alternates`&lt;/span&gt;，将 &lt;span class=&quot;sb&quot;&gt;`/startdir`&lt;/span&gt; 替换为宿主机实际路径。
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 4. 常见问题排查思路 (Heuristics)&lt;/span&gt;

不要直接给出硬编码的修复代码，而是引导用户分析：
&lt;span class=&quot;p&quot;&gt;
*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**Soname 变更**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   检查日志中是否有 &lt;span class=&quot;sb&quot;&gt;`==&amp;gt; Sonames differ in ...`&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   使用 &lt;span class=&quot;sb&quot;&gt;`sogrep-loong64 -r all &amp;lt;lib.so&amp;gt;`&lt;/span&gt; 查找受影响的包。
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**链接器错误 (Linker Errors)**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   如果是 LTO 期间的段错误或 &lt;span class=&quot;sb&quot;&gt;`R_LARCH_B26`&lt;/span&gt; 溢出，考虑是否因为某些原因没有覆盖到 &lt;span class=&quot;sb&quot;&gt;`-mcmodel=medium`&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   也可能是链接器的 Bug，也许可以尝试切换链接器（如 &lt;span class=&quot;sb&quot;&gt;`mold`&lt;/span&gt;）或临时禁用 LTO (&lt;span class=&quot;sb&quot;&gt;`options=(!lto)`&lt;/span&gt;) 作为验证手段，而非永久修改。
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**QEMU User 特异性问题**&lt;/span&gt;（仅对于在 QEMU User 模式下构建的包，在真机上运行时请忽略）：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   如果遇到诡异的卡死、超时或 &lt;span class=&quot;sb&quot;&gt;`multiprocessing`&lt;/span&gt; 失败，提醒用户这可能是 QEMU User 模式的限制。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   建议在 QEMU System 或真机上复现以排除模拟层干扰。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   如果在真机上，即运行在 LoongArch64 硬件上，忽略 QEMU 相关的调试建议。
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   &lt;span class=&quot;gs&quot;&gt;**补丁冲突 (Patch Conflicts)**&lt;/span&gt;：
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   在维护 &lt;span class=&quot;sb&quot;&gt;`loong.patch`&lt;/span&gt; 时，&lt;span class=&quot;gs&quot;&gt;**优先使用追加 (`+=`) 而非修改原数组**&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;    *&lt;/span&gt;   对于不需要的代码块，使用&lt;span class=&quot;gs&quot;&gt;**多行注释**&lt;/span&gt; (&lt;span class=&quot;sb&quot;&gt;`: &amp;lt;&amp;lt;COMMENT ... COMMENT`&lt;/span&gt;) 包裹，避免直接删除导致上游更新时冲突。
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;### 4.1 PKGBUILD 补丁策略详解：将冲突降至最低&lt;/span&gt;

在维护 &lt;span class=&quot;sb&quot;&gt;`loong.patch`&lt;/span&gt;（针对 Arch Linux 官方 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 的补丁集）时，&lt;span class=&quot;gs&quot;&gt;**核心原则是避免与上游日常更新产生 merge conflict**&lt;/span&gt;。由于我们维护的是补丁而非直接修改上游仓库，任何对原数组的直接修改都可能在上游升级时导致补丁失效。

&lt;span class=&quot;gu&quot;&gt;#### 为什么优先追加而非修改？&lt;/span&gt;

上游维护者通常按字母顺序或逻辑分组来组织依赖数组。如果你直接在数组中间插入新元素，上游一旦调整顺序或添加新依赖，你的补丁就会冲突。

&lt;span class=&quot;gs&quot;&gt;**正确做法：**&lt;/span&gt; 在 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 文件末尾使用 &lt;span class=&quot;sb&quot;&gt;`+=`&lt;/span&gt; 追加元素，这样无论上游如何修改原数组，你的补丁都能稳定应用。

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 错误：直接修改原数组（容易冲突）&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;upstream.tar.gz&apos;&lt;/span&gt;
        &lt;span class=&quot;s1&quot;&gt;&apos;loongarch-fix.patch&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# 插入在这里，上游改顺序就冲突&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 正确：在文件末尾追加&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt;+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;loongarch-fix.patch&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
sha256sums+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;xxx&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# 实际哈希值&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

同样的原则适用于其他数组：
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 在 PKGBUILD 末尾追加&lt;/span&gt;
makedepends+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;mold&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
depends+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;some-loong64-specific-dep&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
options+&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;!lto&apos;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;#### 处理大段删除：使用多行注释&lt;/span&gt;

如果某个功能（如 CUDA 支持）在 loong64 上不可用，&lt;span class=&quot;gs&quot;&gt;**直接删除代码会导致上游修改该函数时补丁冲突**&lt;/span&gt;。

&lt;span class=&quot;gs&quot;&gt;**正确做法：**&lt;/span&gt; 使用 Bash 的多行注释语法 &lt;span class=&quot;sb&quot;&gt;`: &amp;lt;&amp;lt;COMMENT_SEPARATOR ... COMMENT_SEPARATOR`&lt;/span&gt; 包裹不需要的代码：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 在 PKGBUILD 中&lt;/span&gt;
build&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;# ... 正常构建逻辑 ...&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;# Use a &quot;multi-line comment&quot; to keep patch from rotting&lt;/span&gt;
    : &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;COMMENT_SEPARATOR&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    CFLAGS=&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CFLAGS&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -fno-lto&quot; CXXFLAGS=&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CXXFLAGS&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -fno-lto&quot; LDFLAGS=&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;LDFLAGS&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -fno-lto&quot; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    cmake -B build-cuda -S &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkgname&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$_opts&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
        -DCUDA_ARCH_BIN=&apos;...&apos; &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
        -DCUDA_ARCH_PTX=&apos;...&apos;
    cmake --build build-cuda
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;COMMENT_SEPARATOR
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

如果不需要构建的内容在函数尾部，也可以直接插入 &lt;span class=&quot;sb&quot;&gt;`return`&lt;/span&gt; 提前退出函数。

&lt;span class=&quot;gu&quot;&gt;#### 动态过滤数组元素&lt;/span&gt;

对于 &lt;span class=&quot;sb&quot;&gt;`pkgname`&lt;/span&gt;、&lt;span class=&quot;sb&quot;&gt;`depends`&lt;/span&gt;、&lt;span class=&quot;sb&quot;&gt;`makedepends`&lt;/span&gt; 等经常变动的数组，即使使用 &lt;span class=&quot;sb&quot;&gt;`+=`&lt;/span&gt; 也无法解决&quot;需要移除某些元素&quot;的情况。&lt;span class=&quot;gs&quot;&gt;**直接删除同样会冲突**&lt;/span&gt;。

&lt;span class=&quot;gs&quot;&gt;**正确做法：**&lt;/span&gt; 在 &lt;span class=&quot;sb&quot;&gt;`PKGBUILD`&lt;/span&gt; 末尾使用 &lt;span class=&quot;sb&quot;&gt;`grep -Ev`&lt;/span&gt; 动态过滤：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# 从 pkgname 中移除不需要的子包&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;pkgname&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;pkgname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ev&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^(torchvision-cuda|python-torchvision-cuda)$&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 从 makedepends 中移除不需要的依赖&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;makedepends&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=(&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;%s&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;makedepends&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[@]&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ev&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^(cuda|cudnn|python-pytorch-opt-cuda)$&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

这种方式的优势：
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; 使用正则表达式 &lt;span class=&quot;sb&quot;&gt;`^`&lt;/span&gt; 和 &lt;span class=&quot;sb&quot;&gt;`$`&lt;/span&gt; 精确匹配，避免误删
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; 灵活适配上游对数组顺序或内容的任何修改
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; 补丁本身不依赖上游数组的具体结构

&lt;span class=&quot;gu&quot;&gt;#### 处理 `pkgver()` 函数&lt;/span&gt;

如果 &lt;span class=&quot;sb&quot;&gt;`pkgver()`&lt;/span&gt; 函数因环境差异导致版本号不一致（如 git hash 位数不同）：
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**不要**&lt;/span&gt;在 &lt;span class=&quot;sb&quot;&gt;`loong.patch`&lt;/span&gt; 中修改 &lt;span class=&quot;sb&quot;&gt;`pkgver`&lt;/span&gt; 变量
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**应该**&lt;/span&gt;注释掉 &lt;span class=&quot;sb&quot;&gt;`pkgver()`&lt;/span&gt; 函数，使用上游提交的版本号
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**更好的做法**&lt;/span&gt;：向上游反馈，要求在 &lt;span class=&quot;sb&quot;&gt;`git describe`&lt;/span&gt; 中添加 &lt;span class=&quot;sb&quot;&gt;`--abbrev=12`&lt;/span&gt; 等参数以保证一致性（以&quot;增加构建可复现性&quot;为由）
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 6. 本地仓库 (Local Repo)：有依赖关系的顺序构建&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 6.1 使用场景与作用&lt;/span&gt;

在移植过程中，经常会遇到以下场景：
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Soname 变更后的连锁重构**&lt;/span&gt;：上游升级导致 soname 变化，需要重新构建链接到它的包。这些包之间存在依赖关系，必须先构建并安装前面的包，后面的包才能正确链接。
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**Bootstrap 引导链**&lt;/span&gt;：某些包需要自身旧版本才能构建新版本，形成自举依赖。
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**批量打包后一次性上传**&lt;/span&gt;：社区规范要求依赖关系完整的包组必须一并上传，不能中途上传半成品到正式仓库。

&lt;span class=&quot;gs&quot;&gt;**本地仓库的作用**&lt;/span&gt;：在本地创建一个临时的 pacman 软件源，按构建顺序逐个添加已打包的软件包，使后续包的构建过程能自动安装前面已构建好的依赖。这比手动传递 &lt;span class=&quot;sb&quot;&gt;`-I`&lt;/span&gt; 参数给 &lt;span class=&quot;sb&quot;&gt;`makechrootpkg`&lt;/span&gt; 更可靠、更可扩展。

&lt;span class=&quot;gu&quot;&gt;### 6.2 建立本地仓库&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /srv/local-repo
&lt;span class=&quot;nb&quot;&gt;sudo chown&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$USER&lt;/span&gt;:alpm /srv/local-repo
repo-add /srv/local-repo/local-repo.db.tar.gz
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 6.3 添加软件包到本地仓库&lt;/span&gt;

一个 &lt;span class=&quot;sb&quot;&gt;`pkgbase`&lt;/span&gt; 构建完成后，其产生的&lt;span class=&quot;gs&quot;&gt;**所有**&lt;/span&gt; &lt;span class=&quot;sb&quot;&gt;`pkgname`&lt;/span&gt; 包（包括 debug 包以外的所有产物）必须&lt;span class=&quot;gs&quot;&gt;**同时添加**&lt;/span&gt;到本地仓库。为简化操作，可封装脚本：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# add-to-local: 批量添加包到本地仓库&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Usage: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$0&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;lt;db-file&amp;gt; &amp;lt;package1&amp;gt; [package2] ...&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;0
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CURRENT_DIR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;DB_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;DB_DIR&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;dirname&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;shift
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;pkg &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$@&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;pkg_basename&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkg&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkg&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DB_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkg_basename&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-debug-&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Found debug package: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkg_basename&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;continue
  fi
  &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DB_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  repo-add &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$DB_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$pkg_basename&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CURRENT_DIR&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

使用方式：
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;add-to-local /srv/local-repo/local-repo.db.tar.gz /path/to/pkgbase/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.pkg.tar.zst
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 6.4 在 archbuild 中启用本地仓库&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
0.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**先检查是否已启用，已启用就不要重复修改，跳过之后的步骤**&lt;/span&gt;：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CONF&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/usr/share/devtools/pacman.conf.d/local-loong64.conf
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONF&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-q&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;^\[local-repo\]&apos;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$CONF&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; /usr/bin/local-loong64-build &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;local-repo 已启用，跳过重复修改&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**创建 pacman 配置**&lt;/span&gt;：在 &lt;span class=&quot;sb&quot;&gt;`/usr/share/devtools/pacman.conf.d/`&lt;/span&gt; 下创建配置文件（如 &lt;span class=&quot;sb&quot;&gt;`local-loong64.conf`&lt;/span&gt;），从 &lt;span class=&quot;sb&quot;&gt;`extra-loong64.conf`&lt;/span&gt; 复制并编辑：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; /usr/share/devtools/pacman.conf.d/extra-loong64.conf &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
   /usr/share/devtools/pacman.conf.d/local-loong64.conf
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
2.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**在配置文件中插入本地源**&lt;/span&gt;（&lt;span class=&quot;gs&quot;&gt;**必须在所有其他源之前**&lt;/span&gt;）：

如果配置里已经存在 &lt;span class=&quot;sb&quot;&gt;`[local-repo]`&lt;/span&gt; 段，&lt;span class=&quot;gs&quot;&gt;**不要重复插入**&lt;/span&gt;。

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;conf
&lt;/span&gt;[&lt;span class=&quot;n&quot;&gt;local&lt;/span&gt;-&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;]
&lt;span class=&quot;n&quot;&gt;SigLevel&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;Never&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Server&lt;/span&gt; = &lt;span class=&quot;n&quot;&gt;file&lt;/span&gt;:///&lt;span class=&quot;n&quot;&gt;srv&lt;/span&gt;/&lt;span class=&quot;n&quot;&gt;local&lt;/span&gt;-&lt;span class=&quot;n&quot;&gt;repo&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 下面是原有的源...
&lt;/span&gt;[&lt;span class=&quot;n&quot;&gt;core&lt;/span&gt;-&lt;span class=&quot;n&quot;&gt;loong64&lt;/span&gt;]
...
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
3.&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**创建 archbuild 软链接**&lt;/span&gt;（名称必须与 conf 文件对应）：

如果软链接已经存在，&lt;span class=&quot;gs&quot;&gt;**不要重复创建**&lt;/span&gt;；可先检查再创建：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; /usr/bin/local-loong64-build &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo ln&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /usr/bin/archbuild /usr/bin/local-loong64-build
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

之后即可使用 &lt;span class=&quot;sb&quot;&gt;`local-loong64-build`&lt;/span&gt; 命令进行构建，它会自动从本地仓库获取已构建好的依赖。

&lt;span class=&quot;gu&quot;&gt;### 6.5 完整构建流程示例&lt;/span&gt;

使用 &lt;span class=&quot;sb&quot;&gt;`genrebuild`&lt;/span&gt; 获取构建顺序后，按顺序逐个构建并添加到本地仓库：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Bash 示例&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;pkg &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;package1 package2 package3 ...&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;get-loong64-pkg &lt;span class=&quot;nv&quot;&gt;$pkg&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-update&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$pkg&lt;/span&gt;
    gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; keys/pgp/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; updpkgsums&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; :&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;done
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.pkg.tar.zst&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ..
    get-loong64-pkg &lt;span class=&quot;nv&quot;&gt;$pkg&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-update&lt;/span&gt;
    script &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;time local-loong64-build -- -- -A&quot;&lt;/span&gt; build-log-all.log &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
        ../add-to-local /srv/local-repo/local-repo.db.tar.gz &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;.pkg.tar.zst
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-ne&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;break
    &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fi
done&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gs&quot;&gt;**全部构建成功后**&lt;/span&gt;，再将所有软件包一次性上传到正式仓库。完成后清空本地仓库：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rf&lt;/span&gt; /srv/local-repo/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
repo-add /srv/local-repo/local-repo.db.tar.gz
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;### 6.6 注意事项&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**版本锁定问题**&lt;/span&gt;：如果本地仓库中有更新的包，但某个依赖锁定了旧版本，可能导致构建无效。可用 &lt;span class=&quot;sb&quot;&gt;`pactree`&lt;/span&gt; 排查依赖关系。
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**上游 conf 更新后**&lt;/span&gt;：当 &lt;span class=&quot;sb&quot;&gt;`devtools-loong64`&lt;/span&gt; 的 conf 文件更新后，需重新编辑 &lt;span class=&quot;sb&quot;&gt;`local-loong64.conf`&lt;/span&gt; 同步上游变化。
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;gs&quot;&gt;**`pkgrel` 批量修改**&lt;/span&gt;：按规范自动将 &lt;span class=&quot;sb&quot;&gt;`pkgrel`&lt;/span&gt; 小数部分加 1：
  &lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;  perl &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-pe&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;s{(^[^#]*?\bpkgrel\s*=\s*)(\d+)(?:\.(\d+))?}{$1 . $2 . &quot;.&quot; . (defined($3) ? $3 + 1 : 1)}e&apos;&lt;/span&gt; PKGBUILD
  &lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## 7. 标准构建命令&lt;/span&gt;

推荐用户使用 &lt;span class=&quot;sb&quot;&gt;`script`&lt;/span&gt; 记录完整日志（包含 stderr 中的 &lt;span class=&quot;sb&quot;&gt;`checkpkg`&lt;/span&gt; 输出）：

&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;bash
&lt;/span&gt;script &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;time extra-loong64-build $(bash -c &quot;source PKGBUILD; [[ \&quot; \${arch[*]} \&quot; =~ \&quot; loong64 \&quot; ]] || echo -- -- -A&quot;)&apos;&lt;/span&gt; build-log-all.log
&lt;span class=&quot;p&quot;&gt;```&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
*&lt;/span&gt;   如果是本地仓库构建，将 &lt;span class=&quot;sb&quot;&gt;`extra-loong64-build`&lt;/span&gt; 替换为 &lt;span class=&quot;sb&quot;&gt;`local-loong64-build`&lt;/span&gt;。
&lt;span class=&quot;p&quot;&gt;*&lt;/span&gt;   参数 &lt;span class=&quot;sb&quot;&gt;`-- -- -A`&lt;/span&gt; 用于在非 loong64 架构宿主机上强制构建 loong64 包（通过 QEMU User）。
&lt;span class=&quot;p&quot;&gt;
---
&lt;/span&gt;
&lt;span class=&quot;gu&quot;&gt;## License&lt;/span&gt;

本 Skill 文件（&lt;span class=&quot;sb&quot;&gt;`SKILL.md`&lt;/span&gt;）采用 &lt;span class=&quot;gs&quot;&gt;**Creative Commons Attribution-ShareAlike 4.0 International (CC-BY-SA-4.0)**&lt;/span&gt; 发布。

完整许可证文本见 &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;CC BY-SA 4.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;](&lt;/span&gt;&lt;span class=&quot;sx&quot;&gt;https://creativecommons.org/licenses/by-sa/4.0/&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/04/04/archlinux-loong64-portbuild-skill/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/04/04/archlinux-loong64-portbuild-skill/</guid>
        
        <category>开源软件</category>
        
        <category>国产硬件</category>
        
        <category>AI</category>
        
        <category>LoongArchLinux</category>
        
        <category>LLM</category>
        
        <category>archlinux</category>
        
        
      </item>
    
      <item>
        <title>PvZ-Portable 输入系统与主循环优化：原生字符合成与 Web 端延迟修复</title>
        <description>&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;在之前完成&lt;a href=&quot;https://wszqkzqk.github.io/2026/03/10/PvZ-Portable-WebAssembly-Adaptation/&quot;&gt;基于 Emscripten 的浏览器端适配&lt;/a&gt;后，PvZ-Portable 终于填补了跨平台版图的最后一块。引擎此时已经能在 PC、移动端乃至纯 Web 环境中跑通基础流程。但对于游戏移植而言，完成编译只是第一步，真正的考验在于如何抹平不同宿主环境对底层设施的入侵感。&lt;/p&gt;

&lt;p&gt;在不同平台的细微体验打磨中，应用的输入处理和事件调度常常是重灾区。近期，笔者主要对 PvZ-Portable 的底层事件分发以及 Web 端主循环做了一次针对性的重构修复，进一步解决了跨系统开发中输入响应迟滞和事件派发错位等问题。本文将重点介这两项底层基础设施的优化细节。&lt;/p&gt;

&lt;h2 id=&quot;emscripten-主循环事件漏帧与鼠标拖动卡顿修复&quot;&gt;Emscripten 主循环事件漏帧与鼠标拖动卡顿修复&lt;/h2&gt;

&lt;p&gt;在通过 Emscripten 编译到 Web 平台时，受限于浏览器的执行模型，传统的无限阻塞式游戏主循环必须被转换为异步的回调机制，通常挂载到浏览器的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requestAnimationFrame&lt;/code&gt;（rAF）中。&lt;/p&gt;

&lt;p&gt;为了适配这一机制，PvZ-Portable 将原有的进程阻塞转编为一套状态机机制，通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UpdateAppStep&lt;/code&gt; 函数逐步推进应用状态。然而，在之前的代码实现中，主循环的回调存在一个导致输入延迟和偶发掉帧的判定问题，其最典型的症状是：&lt;strong&gt;在 Web 端游戏内拖动鼠标时，画面会出现明显的卡顿甚至完全冻结。&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 旧版轮询逻辑&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mUpdateAppState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UPDATESTATE_PROCESS_2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mHasPendingDraw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UpdateAppStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mUpdateAppState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UPDATESTATE_PROCESS_DONE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mHasPendingDraw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这段代码的问题在于对其状态穷举不足。在特定的中间状态下，如果在当前迭代中没有产生明显的逻辑更新（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated&lt;/code&gt; 为 false），循环会提前 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;break&lt;/code&gt;，将剩余的事件处理或帧渲染推迟到下一个 rAF 回调。&lt;/p&gt;

&lt;p&gt;当玩家快速移动或拖动鼠标时，底层会产生大量的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_MOUSEMOTION&lt;/code&gt; 事件，而这种提前 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;break&lt;/code&gt; 的机制导致引擎无法在一个单独的浏览器刷新帧内将堆积的鼠标事件完全消化。未处理的事件导致事件队列拥塞，状态机因为事件迟滞而无法推进，反映在游戏体验上便是严重的拖放卡顿感。&lt;/p&gt;

&lt;p&gt;优化方案是将终止条件改为显式校对最终完成状态，强制事件泵在单次浏览器帧回调内彻底排空所有挂起阶段和积压事件：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 优化后的 Emscripten 轮询逻辑&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mUpdateAppState&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;UPDATESTATE_PROCESS_DONE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mHasPendingDraw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UpdateAppStep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;通过这一改动，只要引擎没有抵达 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATESTATE_PROCESS_DONE&lt;/code&gt;，状态机就会在其所在的这一个 rAF 内部不间断推进。所有的用户输入堆栈（包括高频次触发的鼠标移动操作）和挂起的绘制指令都能在一个物理周期内高效率地提取并结算。更新此逻辑后，Web 端鼠标拖放的卡顿问题得到了彻底解决。&lt;/p&gt;

&lt;h2 id=&quot;sdl2-原生字符合成与作弊快捷键修复&quot;&gt;SDL2 原生字符合成与作弊快捷键修复&lt;/h2&gt;

&lt;p&gt;原版游戏及引擎中存在许多依赖于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KeyChar&lt;/code&gt; 键盘事件直接触发的动作交互，其中就包含了游戏内丰富的作弊快捷键（&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-DPVZ_DEBUG&lt;/code&gt;且启动时传递&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-tod&lt;/code&gt;参数时开启）。&lt;/p&gt;

&lt;p&gt;在现代的跨平台 SDL2 开发范式中，上层若想获取标准的对应字符级事件，常规的做法是调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_StartTextInput()&lt;/code&gt;。但这在许多非传统桌面系统平台中会引发不符合预期的副作用：无论是在移动设备上还是包含虚拟键盘的现代桌面环境，拉起标准的 Text Input 上下文会不可避免地强制唤起输入法辅助面板，甚至直接拉长屏幕占用遮挡半个游戏视野。&lt;/p&gt;

&lt;p&gt;然而我们仅仅是想让玩家在游戏正常流程下顺畅使用那些老式的单键快捷组合。引擎需要一种能在不触发系统输入法弹窗的前提下，静默且准确地提取字符事件的方案。&lt;/p&gt;

&lt;p&gt;由于这部分快捷键纯粹属于字符组合与映射操作，笔者在 SDL 事件泵处理 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_KEYDOWN&lt;/code&gt; 的逻辑中建立了一个底层的 ASCII 字符合成器，从物理按键及其修饰符直接向游戏应用派发所需的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KeyChar&lt;/code&gt; 信息：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SDLSynthesizeAsciiCharFromKeyDown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_KeyboardEvent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theChar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;theChar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 当游戏真正需要文本输入（如存档命名）时，让位给系统的系统输入法&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDL_IsTextInputActive&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;SDL_Keycode&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;SDL_Keymod&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aMods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDL_Keymod&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;theEvent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasCtrl&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aMods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KMOD_CTRL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasAlt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aMods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KMOD_ALT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasGui&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aMods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KMOD_GUI&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasShift&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aMods&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;KMOD_SHIFT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 过滤掉包含 Alt 或 Gui 中继键的操作&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aHasAlt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasGui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 映射字母并处理 Shift 组合&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_z&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;theChar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasCtrl&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;static_cast&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aHasShift&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;A&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aHasCtrl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// 映射数字区域与其他标点符号&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theChar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aHasShift&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;!&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos;1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... (其他字符映射省略)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDLK_SPACE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;theChar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sc&quot;&gt;&apos; &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nl&quot;&gt;default:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;随后在事件循环处理 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_KEYDOWN&lt;/code&gt; 的分支处增加对合成器的调用：&lt;/p&gt;

&lt;div class=&quot;language-cpp highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;SDL_KEYDOWN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mLastUserInputTick&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mLastTimerTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mWidgetManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;KeyDown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLKeyToKeyCode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sym&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;

    &lt;span class=&quot;kt&quot;&gt;char&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSynthesizedChar&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SDLSynthesizeAsciiCharFromKeyDown&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;aSynthesizedChar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;mWidgetManager&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;KeyChar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aSynthesizedChar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在此流程下，当玩家敲击键盘时，普通的字符映射被自动合成为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KeyChar&lt;/code&gt; 信息传递给 WidgetManager 控制层。游戏内部对于键盘流的监听与原版无缝衔接，同时通过判断 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDL_IsTextInputActive()&lt;/code&gt;，保留了玩家建立新存档需要输入账号名称时的正常虚拟键盘唤起通道，互不冲突。&lt;/p&gt;

&lt;h2 id=&quot;结语&quot;&gt;结语&lt;/h2&gt;

&lt;p&gt;不同运行环境的差异往往体现在 API 缝隙以及系统调用习惯上。通过修正 Emscripten 帧回调中的提前退出机制，以及解耦字符获取机制与 OS 输入法组件之间的强绑定，PvZ-Portable 进一步消除了不同平台的体验差异。游戏引擎的基础设施改进仍在继续进行中。&lt;/p&gt;

&lt;h2 id=&quot;️-版权与说明&quot;&gt;⚠️ 版权与说明&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;重要：本项目仅包含代码引擎，不包含任何游戏素材！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PvZ-Portable 严格遵守版权协议。游戏的 IP（植物大战僵尸）属于 PopCap/EA。&lt;/p&gt;

&lt;p&gt;要研究或使用此项目，你&lt;strong&gt;必须&lt;/strong&gt;拥有正版游戏（如果没有，请在 &lt;a href=&quot;https://store.steampowered.com/app/3590/Plants_vs_Zombies_GOTY_Edition/&quot;&gt;Steam&lt;/a&gt; 或 &lt;a href=&quot;https://www.ea.com/games/plants-vs-zombies/plants-vs-zombies&quot;&gt;EA 官网&lt;/a&gt; 上购买）。你需要从正版游戏中提取以下文件放到 PvZ-Portable 的程序所在目录中。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.pak&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;properties/&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本项目的源代码以 &lt;a href=&quot;https://www.gnu.org/licenses/lgpl-3.0.html&quot;&gt;&lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;&lt;/a&gt; 许可证开源，欢迎学习和贡献。&lt;/p&gt;
</description>
        <pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/03/23/PvZ-Portable-UX-Polishing/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/03/23/PvZ-Portable-UX-Polishing/</guid>
        
        <category>C++</category>
        
        <category>SDL2</category>
        
        <category>开源软件</category>
        
        <category>游戏移植</category>
        
        <category>开源游戏</category>
        
        <category>PvZ-Portable</category>
        
        <category>WebAssembly</category>
        
        
      </item>
    
      <item>
        <title>Qt Web Extractor：轻量级的跨平台网页内容提取工具</title>
        <description>&lt;h2 id=&quot;网页内容提取的痛点&quot;&gt;网页内容提取的痛点&lt;/h2&gt;

&lt;p&gt;目前给 LLM 平台提供网页提取或者搜索功能的 API 一般依赖于 Playwright 或 Puppeteer 等技术。这些技术十分强大，能够完美处理动态网页，但也带来了一个显著的问题：过于笨重。&lt;/p&gt;

&lt;p&gt;它们通常要求在使用时下载体积庞大的独立完整浏览器二进制文件，在运行时也需要启动完整的浏览器进程。对于一套仅仅用来抓取文本的后端服务来说，这不仅占用了较多的磁盘存储和执行内存，在不同环境部署时也显得有些麻烦。&lt;/p&gt;

&lt;p&gt;更为棘手的问题在于跨架构的支持。笔者同时担任着 Arch Linux for Loong64 的维护者，在适配 LoongArch 架构时深有体会。这其实是双重的挑战：首先，像 Playwright 这样的工具本身在 LoongArch 下从源码构建就困难重重，笔者至今也没有成功构建出其稳定可用的版本；其次，它们在运行时强依赖于上游官方预编译发布的 Chromium 等独立浏览器二进制文件，而官方并没有提供针对 LoongArch 等非主流架构的打包分支，这让常规的方法很困难。&lt;/p&gt;

&lt;p&gt;在这样的背景下，笔者开发了 &lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;qt-web-extractor&lt;/a&gt; 项目。项目的核心目标是打造一个&lt;strong&gt;简单、易用、轻量且真正跨平台&lt;/strong&gt;的通用网页内容提取工具。在 Linux、Windows、macOS 在内的各平台上都能轻松部署运行，不再受限于强绑定的独立浏览器二进制文件；同时，它也顺带完美解决了前文所述的跨指令集架构难题——可以直接使用发行版提供的 Qt WebEngine 库。&lt;/p&gt;

&lt;h2 id=&quot;为什么选择-qt-webengine&quot;&gt;为什么选择 Qt WebEngine&lt;/h2&gt;

&lt;p&gt;既然需要一个能解析 JavaScript、执行客户端动态渲染并且高度跨平台的轻量级替代方案，Qt WebEngine 成为了极佳的选择。&lt;/p&gt;

&lt;p&gt;Qt WebEngine 本质上是 Chromium 的封装，前端渲染和 JS 执行能力与现代浏览器完全一致。它本身就是一个极其成熟及通用的跨平台基础构件，只需引入对应的 Python 绑定包 PySide6，即可直接作为轻量的提取引擎工作，免去了繁琐的额外配置。&lt;/p&gt;

&lt;p&gt;而在 Linux 平台下，它还额外带来了得天独厚的优势：作为非常通用的基础 GUI 依赖，从 x86 到 LoongArch、RISC-V 等各种指令集的分支，几乎所有的主流发行版都已经把它在软件仓库中打包集成好了。如果在 Linux 下使用，它可以直接复用系统的动态库，无需再去第三方服务器下载动辄好几百兆的 Chromium 内核。只要用常规的包管理器备齐环境，部署本项目几乎不会带来任何额外的存储占用开销。&lt;/p&gt;

&lt;h2 id=&quot;qt-web-extractor-简介&quot;&gt;&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 简介&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 是一个基于 Qt WebEngine 和 Qt PDF 实现的通用网页内容提取工具。它通过 Qt 的 offscreen 模式实现无头运行，不需要显示器或显卡参与渲染，专门用于解决提取那些依赖 JavaScript、由客户端动态加载渲染的现代网页内容，弥补普通 HTTP 请求无法执行 JS 的短板。&lt;/p&gt;

&lt;p&gt;项目仅依赖于 PySide6 与 Qt6 WebEngine 模块，方便部署。&lt;/p&gt;

&lt;h3 id=&quot;核心特性&quot;&gt;核心特性&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;全面的 JavaScript 渲染支持：对于常见的单页应用或客户端渲染构建的页面，普通的 requests 纯 HTTP 请求只能获取基础的 HTML 代码。本项目会等待页面渲染完毕，返回包含实际完整内容的纯文本或 DOM 代码。&lt;/li&gt;
  &lt;li&gt;多种使用接口：提供命令行工具、Python 模块 API，以及内置的 HTTP REST API 服务。&lt;/li&gt;
  &lt;li&gt;通用的 HTTP API：服务端常驻后台后，能为各类 AI 平台、监控脚本等提供统一的页面与文本获取服务。同时，它还开箱即用支持被配置为 &lt;a href=&quot;https://github.com/open-webui/open-webui&quot;&gt;Open WebUI&lt;/a&gt; 的外部网页加载器，源码目录中也包含了作为对话自定义工具引用的脚本。&lt;/li&gt;
  &lt;li&gt;原生 &lt;strong&gt;PDF 解析&lt;/strong&gt;：这个扩展特性针对的是一个非常影响现实体验的痛点。之前笔者发现在 Open WebUI 中，如果 LLM 使用 Open WebUI 自带的&lt;strong&gt;默认提取器&lt;/strong&gt;去调取一个 PDF 类型的链接，会把 PDF 的二进制格式文件直接拉下来，然后当成文本直接塞到对话的上下文中。这可以说是一场灾难，模型不仅获取其中的有效信息，还会浪费巨量 Token。而在本机提取中，遇到 PDF 文件时，可以直接利用与 Qt WebEngine 搭配的 &lt;strong&gt;Qt PDF&lt;/strong&gt; 库将文件中的真正&lt;strong&gt;可读文本层解析、提取&lt;/strong&gt;并返回，完美解决了这个十分影响用户体验的痛点。&lt;/li&gt;
  &lt;li&gt;部署极简：&lt;strong&gt;依赖简洁且不需要独立下载浏览器&lt;/strong&gt;，提供 &lt;strong&gt;systemd 服务&lt;/strong&gt;文件和基于 &lt;a href=&quot;https://aur.archlinux.org/packages/qt-web-extractor&quot;&gt;AUR&lt;/a&gt; 的部署方式，安装和后台启停都非常方便。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;使用方式展示&quot;&gt;使用方式展示&lt;/h3&gt;

&lt;h4 id=&quot;常驻-http-服务&quot;&gt;常驻 HTTP 服务&lt;/h4&gt;

&lt;p&gt;工具最常见的使用方式是作为后端的公共常驻服务。项目内置了 HTTP 服务器：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;qt-web-extractor serve &lt;span class=&quot;nt&quot;&gt;--host&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;nt&quot;&gt;--port&lt;/span&gt; 8766 &lt;span class=&quot;nt&quot;&gt;--api-key&lt;/span&gt; mysecretkey
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;启动以后，它就可以作为一个通用的 REST API 供其他程序调用。例如在 Open WebUI 的管理后台中，将 Web Loader Engine 设置为 external，填写这个地址和配置好的 API Key。此后环境中所有的网页阅读、数据拉取都会交由它进行渲染分析。因为能执行 JS 的缘故，它自然也可以顺利加载更多的网页（比如通过简单 HTTP 访问不能获取的知乎、Hugging Face 等等），并且能在应对含有弹窗或单页路由的页面时获取到渲染后的实际 DOM 文本。&lt;/p&gt;

&lt;h4 id=&quot;命令行工具&quot;&gt;命令行工具&lt;/h4&gt;

&lt;p&gt;通过命令行可以直接快速提取目标源并将结果输出到终端：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 纯文本提取&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; qt_web_extractor https://example.com

&lt;span class=&quot;c&quot;&gt;# 输出 JSON 格式&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; qt_web_extractor &lt;span class=&quot;nt&quot;&gt;--json&lt;/span&gt; https://example.com

&lt;span class=&quot;c&quot;&gt;# 提取渲染后的 HTML 代码&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; qt_web_extractor &lt;span class=&quot;nt&quot;&gt;--html&lt;/span&gt; https://example.com

&lt;span class=&quot;c&quot;&gt;# 解析 PDF 文本&lt;/span&gt;
python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; qt_web_extractor https://example.com/document.pdf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;python-代码调用&quot;&gt;Python 代码调用&lt;/h4&gt;

&lt;p&gt;项目本身也是一个标准的 Python 包：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;qt_web_extractor&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;QtWebExtractor&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;extractor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;QtWebExtractor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timeout_ms&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;extractor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;extract&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;https://example.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 提取经过渲染的网页纯文本
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;html&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 提取渲染后的 HTML
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;工具引擎在主线程运行 Qt 的事件循环，通过后台线程处理 HTTP 接口。面对每次请求，会在网页完成加载后再给出一定的缓冲时间保证 JS 执行完毕，确保提取的是最终视图数据。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;Qt Web Extractor&lt;/a&gt; 通过系统自带的 Qt WebEngine 来进行加载和提取，既保留了执行现代前端框架必备的 Chromium 核心能力，又规避了平台架构支持的局限和额外的存储烦恼。&lt;/p&gt;

&lt;p&gt;如果你也面临特殊指令集的部署难题或者厌倦了总是要下载臃肿的浏览器环境，可以尝试一下这个极简的方案。在 Arch Linux 上可以通过 &lt;a href=&quot;https://aur.archlinux.org/packages/qt-web-extractor&quot;&gt;AUR&lt;/a&gt; 直接安装包。&lt;/p&gt;

&lt;p&gt;项目仓库地址：&lt;a href=&quot;https://github.com/wszqkzqk/qt-web-extractor&quot;&gt;GitHub · Qt Web Extractor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;项目协议：&lt;a href=&quot;https://www.gnu.org/licenses/gpl-3.0.html&quot;&gt;GPL-3.0-or-later&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate>
        <link>http://wszqkzqk.github.io/2026/03/20/qt-web-extractor/</link>
        <guid isPermaLink="true">http://wszqkzqk.github.io/2026/03/20/qt-web-extractor/</guid>
        
        <category>Python</category>
        
        <category>Qt</category>
        
        <category>PySide</category>
        
        <category>开源软件</category>
        
        <category>LLM</category>
        
        
      </item>
    
  </channel>
</rss>
