news 2026/9/14 16:10:58

SDL3 的 MS-DOS 移植版:DJGPP 交叉编译、DPMI 内存模型与 Sound Blaster 音频实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SDL3 的 MS-DOS 移植版:DJGPP 交叉编译、DPMI 内存模型与 Sound Blaster 音频实战指南

SDL3 的 MS-DOS 移植版:DJGPP 交叉编译、DPMI 内存模型与 Sound Blaster 音频实战指南

【免费下载链接】SDLSimple DirectMedia Layer项目地址: https://gitcode.com/GitHub_Trending/sd/SDL

本文档对应仓库中的 docs/README-dos.md,讲解 SDL3 对 MS-DOS 平台的官方移植。该移植基于 DJGPP GCC 交叉编译器生成 32 位保护模式(DPMI)可执行文件,覆盖 VGA/VESA 视频、Sound Blaster 音频、协作式多线程与三类经典输入设备。读完本文,你将掌握在 Linux 上交叉编译 DOS 版 SDL3、在 DOSBox 或真实 DOS 硬件上运行,以及针对 DOS 特性(fat DS 指针、协作式调度、直接帧缓冲)编写和调优应用的全部要点。

移植概览:为什么 DOS 需要一套特殊实现

MS-DOS 是一个 16 位实模式操作系统,没有内存保护、没有内核级线程,也没有现代驱动模型。SDL3 的 DOS 移植采用 DJGPP(GNU GCC 的 DOS 移植)交叉编译,将应用与 SDL3 一起构建为32 位保护模式 DPMI 可执行文件。在仓库中,这一移植的代码分散在几个专门的目录下:

  • 构建工具链:build-scripts/i586-pc-msdosdjgpp.cmake
  • 核心运行时:src/core/dos/SDL_dos.c、src/core/dos/SDL_dos_scheduler.c
  • 视频驱动:src/video/dos/(SDL_dosvideo.c、SDL_dosmodes.c、SDL_dosframebuffer.c、SDL_dosmouse.c、SDL_dosevents.c)
  • 音频驱动:src/audio/dos/SDL_dosaudio_sb.c

由于 DOS 环境的特殊性,这套移植在内存访问、线程调度、视频刷新、音频中断等层面都与桌面平台的实现有本质差异,下面逐一展开。

交叉编译:从 Linux 构建 DOS 可执行文件

前置条件

构建 DOS 版 SDL3 需要:

  1. 下载并解压 DJGPP 工具链;
  2. 将包含i586-pc-msdosdjgpp-gcc的目录加入PATH环境变量;
  3. 保证系统已安装 CMake。

工具链文件会按顺序查找两个编译器:i586-pc-msdosdjgpp-gcci386-pc-msdosdjgpp-gcc(C++ 编译器对应-g++),找不到则直接报错终止配置。从源码看(build-scripts/i586-pc-msdosdjgpp.cmake),i586变体优先。

构建命令

cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=build-scripts/i586-pc-msdosdjgpp.cmake -DCMAKE_BUILD_TYPE=Release cmake --build build

工具链文件的关键行为

这个 toolchain 文件除了选择编译器,还处理了几件对 DOS 构建至关重要的事:

  • 库命名约定覆盖:CMake 自带的Platform/DOS.cmake默认采用 OpenWatcom 命名习惯(无前缀、.lib后缀),而 DJGPP 遵循 Unix/GCC 惯例(lib前缀、.a后缀,如libm.a)。工具链通过CMAKE_USER_MAKE_RULES_OVERRIDE在平台模块之后覆盖这些变量(第 5-13 行),并设置输出后缀.exe
  • 查找根路径:通过执行gcc -print-search-dirs自动推导 DJGPP 的头文件与库文件根目录,追加到CMAKE_FIND_ROOT_PATH(第 42-68 行)。
  • 搜索模式:程序(编译器本身)在宿主目录查找(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER),而库、头文件和包只在目标目录查找(ONLY),避免误链宿主系统的库(第 71-81 行)。

运行时环境与 DPMI 要求

DOS 可执行文件运行在保护模式下,因此必须有一个DPMI host(DOS Protected Mode Interface 宿主)负责提供实模式/保护模式切换、内存锁页等服务。移植文档明确要求:

CWSDPMI.EXE放在可执行文件旁边。

CWSDPMI是 DJGPP 自带的 DPMI 服务程序。在 DOSBox 中运行只需:

dosbox myapp.exe

DOSBox 内置了 DPMI 支持,通常无需额外安装 CWSDPMI,但保持二者兼容无误仍是好习惯。

系统要求

组件最低要求
CPUi386 或更高
RAM4 MB
视频VGA(256 色模式 13h)
音频Sound Blaster
DPMICWSDPMI.exe 或兼容的 DPMI host

更高的分辨率(640×480 及以上)需要VESA VBE 1.2+ 兼容的显卡。这是硬性门槛:视频驱动在初始化时通过 VBE 中断(0x10,功能0x4F00查询 VESA 信息)探测显卡能力,不满足要求就返回错误。从 src/video/dos/SDL_dosvideo.c 可以看到,驱动创建时首先调用DOSVESA_SupportsVESA(),失败则直接返回 NULL,SDL 会以清晰错误提示退出。

内存模型:fat DS nearptr 技巧

DOS 移植的 32 位保护模式程序无法直接访问低于 1 MB 的物理地址(VGA 帧缓冲、DMA 缓冲都位于实模式地址空间)。解决方案是 DJGPP 的"fat DS" nearptr 技巧:调用__djgpp_nearptr_enable()后,DS 段寄存器被设置为"扁平"覆盖整个 4 GB 地址空间,物理地址可以直接当作 C 指针使用,无需手工处理段描述符。

这套机制在 SDL 内部通过DOS_PhysicalToLinear()封装(见 src/core/dos/SDL_dos.c 等调用),被用于:

  • 直接访问 VESA 线性帧缓冲(LFB);
  • 访问常规内存中分配的 DMA 缓冲;
  • 访问实模式中断返回的远指针字符串(DOS_GetFarPtrCString通过_farpeekb逐字节读出)。

关键注意事项:谁负责启用 fat DS

SDL 在 DOS 上强制要求启用 fat DS:

  • 如果应用走标准的SDL_RunApp()入口(SDL_main.h提供),SDL 会自动调用__djgpp_nearptr_enable()
  • 如果应用定义了SDL_MAIN_HANDLED自行接管main(),则必须自己在初始化 SDL 之前调用__djgpp_nearptr_enable(),否则视频初始化会失败并给出明确的错误信息。

这一点在源码中有直接佐证——src/video/dos/SDL_dosvideo.c 在VideoInit阶段检查:

if (__djgpp_conventional_base == 0) { return SDL_SetError("DOSVESA: __djgpp_nearptr_enable() was not called. " "Did you define SDL_MAIN_HANDLED without enabling the fat DS trick?"); }

__djgpp_conventional_base非零即表示 fat DS 已激活。因此,自定义入口的应用请务必按如下方式组织代码:

#define SDL_MAIN_HANDLED #include <SDL3/SDL.h> #include <sys/nearptr.h> // DJGPP 的 nearptr 支持 int main(int argc, char *argv[]) { if (!__djgpp_nearptr_enable()) { return -1; // 无法启用 fat DS,后续视频初始化必然失败 } if (!SDL_Init(SDL_INIT_VIDEO)) { return -1; } // ... 正常 SDL 代码 ... SDL_Quit(); return 0; }

协作式线程调度:setjmp/longjmp 迷你调度器

DOS 没有操作系统级线程。SDL3 的 DOS 移植在 src/core/dos/SDL_dos_scheduler.c 中实现了一个协作式调度器

  • 基于setjmp/longjmp保存和恢复 CPU 上下文;
  • 静态线程表(最多 16 个线程,DOS_MAX_THREADS),默认栈大小 64 KB(见 src/core/dos/SDL_dos_scheduler.h);
  • 轮转(round-robin)调度:FindNextRunnable()从当前线程之后线性扫描READY状态线程;
  • 线程绝不会在指令中途被抢占,切换只发生在显式的让出点。

让出点与主循环要求

上下文切换只发生在SDL_Delay和事件泵等显式让出点,因此文档强调:

请确保主循环定期调用SDL_PumpEventsSDL_Delay

具体建议:

  • SDL_Delay(0):让出 CPU 给其他线程但不实际睡眠,适合在紧密循环中调用;
  • SDL_Delay(16):对于加载画面这类场景,较长的延时可以让后台线程获得足够的 CPU 时间;
  • 音频线程会持续通过DOS_Yield()让出执行权(见下文音频部分),与主线程协作运转。

实现层面的细节

从源码看,调度器的健壮性设计值得一提:

  • 栈内存锁页DOS_CreateThread通过_go32_dpmi_lock_data(stack, stack_size)锁定新线程的栈,避免切换栈时发生缺页异常(SDL_dos_scheduler.c 第 146 行);
  • 上下文引导:新线程的jmp_buf__esp/__ebp被手工指向新栈顶(16 字节对齐并留出伪返回地址),__eip指向ThreadTrampoline,由它调用用户线程函数并处理退出(第 159-182 行);
  • join/block/wake:支持DOS_JoinThreadDOS_BlockCurrentThread(等待信号量)与DOS_WakeThread,构成完整的线程同步原语(第 257-302 行)。

视频:VGA 13h 与 VESA VBE

支持的显示模式

视频驱动(SDLDOSVESA)支持两类模式:

  1. VGA 模式 13h(320×200×256):任何 VGA 显卡都支持,作为兜底模式。由于多数 VESA BIOS 不把 13h 列入模式列表,代码中用哨兵值VGA_MODE_13H_SENTINEL标识并回退到0xA000段(见 src/video/dos/SDL_dosmodes.c);
  2. VESA 扩展模式(VBE 1.2+):支持 640×480 及更高分辨率,同时支持线性帧缓冲(VBE 2.0+ LFB)分段银行(banked)两种访问方式。

只有软件渲染,没有 GPU 渲染器。所有视频模式实际上都是全屏的——驱动在创建窗口时通过SDL_GetClosestFullscreenDisplayMode选择最接近请求尺寸的可用模式(src/video/dos/SDL_dosvideo.c),device_caps被标记为VIDEO_DEVICE_CAPS_FULLSCREEN_ONLY(第 343 行)。默认桌面模式目标为 640×480,应用若想要其他尺寸应显式调用SDL_SetWindowFullscreenMode

颜色模式

8 位索引色(INDEX8)模式使用可编程的 VGA DAC 调色板,但只有显式通过SDL_PIXELFORMAT窗口创建属性请求时才会被采用。调色板更新时机经过精心设计:在普通路径下,DAC 编程被推迟到垂直消隐期(vblank)执行,以避免可见的调色板闪烁(见 src/video/dos/SDL_dosframebuffer.c 的注释与ProgramVGADAC实现)。

硬件检测与退出恢复

  • EGA/CGA 不支持:驱动初始化时检测 VGA 硬件,不存在则失败并给出明确错误信息;
  • 干净退出VideoQuit通过 VBE 中断0x4F04(保存/恢复 VBE 状态)在退出时完整恢复显卡状态;若原模式不是 VBE 模式,则回退到标准文本模式0x03(BIOS 中断0x10,功能0x0003),确保退出后回到正常的 DOS 命令行界面(src/video/dos/SDL_dosvideo.c)。

硬件页翻转(Page Flipping)

当 LFB 可用且显存足够时,驱动使用硬件页翻转实现无撕裂渲染:先绘制到后台页,再通过 VBE 功能0x4F07(子功能0x0080,设置显示起始行)一次性切换显示页。可选 vsync(通过SDL_SetWindowFramebufferVSync设置)在翻转前等待 vblank(见WaitForVBlank,轮询 VGA 状态端口)。

直接帧缓冲 Hint:为 Quake 这类自绘渲染器而生

SDL_HINT_DOS_ALLOW_DIRECT_FRAMEBUFFER"SDL_DOS_ALLOW_DIRECT_FRAMEBUFFER",自 SDL 3.6.0 起可用,见 include/SDL3/SDL_hints.h)开启一条绕过常规 surface 拷贝的快速路径:

SDL_SetHint(SDL_HINT_DOS_ALLOW_DIRECT_FRAMEBUFFER, "1"); SDL_Window *win = SDL_CreateWindow("quake-like", 320, 200, 0); SDL_Surface *surf = SDL_GetWindowSurface(win); // hint 必须在这之前设置

设置后,SDL_UpdateWindowSurface()直接把系统内存中的 surface 拷入 VRAM:

  • 首选通过dosmemput经 VGA 银行窗口(0xA0000)写入——真实硬件测试表明它比 nearptr 写 DPMI 映射的 LFB 明显更快,即使高分辨率下需要切换银行(见 src/video/dos/SDL_dosframebuffer.c 的注释);
  • 仅当没有可用银行窗口时才回退到 nearptr LFB 路径;
  • 只在调色板变化时编程 VGA DAC;
  • 页翻转不做 vblank 等待;
  • 不进行软件光标合成

该模式是为 Quake 这类自己管理渲染、追求极限帧吞吐的应用设计的,代价是:

  • 无 vsync,撕裂是预期行为
  • 无软件光标,应用需自行绘制光标;
  • 在真实硬件上读回 surface 可能很慢(VRAM 未缓存)。

关键约束:hint 必须在第一次调用SDL_GetWindowSurface()之前设置,之后修改无效。从实现看,该 hint 在DOSVESA_CreateWindowFramebuffer中被读取(SDL_dosframebuffer.c 第 274 行),一旦 framebuffer 已按普通路径创建,切换不会生效。

音频:Sound Blaster 家族与 IRQ 驱动环形缓冲

支持的硬件与格式

音频驱动(SDL_AUDIO_DRIVER_DOS_SOUNDBLASTER)支持三代 Sound Blaster:

硬件DSP 版本格式
SB16DSP ≥ 4.x16 位立体声有符号(S16LE),5–44.1 kHz
SB ProDSP 3.x8 位立体声无符号(U8),最高 22050 Hz(立体声)
SB 2.0 / 1.xDSP 2.x / 1.x8 位单声道无符号(U8)

硬件配置完全自动:从BLASTER环境变量解析。解析器(IsSoundBlasterPresent)识别A(基端口,十六进制)、I(IRQ)、D(8 位 DMA 通道)、H(16 位高 DMA 通道)等关键字,忽略M(混音芯片端口)、P(MPU-401)、T(设备类型)等无关项(src/audio/dos/SDL_dosaudio_sb.c)。缺失或错误的变量会返回明确错误。典型配置形如:

SET BLASTER=A220 I5 D1 H5 T6

检测流程还会通过 DSP 复位握手和版本查询(命令0xE1)确认端口上确实是 Sound Blaster,而非其他设备。

环形缓冲架构:约 45 ms 的余量

音频路径采用IRQ 驱动的环形缓冲,这是移植文档强调的核心设计:

  1. SDL 音频线程(协作式)通过完整的 SDL 音频管线(含分配、互斥)将混音结果写入环形缓冲;
  2. Sound Blaster IRQ 处理器在中断上下文中只做一件事:从环形缓冲拷贝一个 chunk 到 DMA 半缓冲(纯memcpy+ 端口 I/O,不碰 DPMI、不分配、不加锁、不用 FPU);
  3. 环形缓冲容量为4 个 chunkRING_BUFFER_CHUNKS 4),在 44100 Hz 下约等于45 ms的缓冲余量——足以覆盖 22 fps 的帧时间。

中断处理器和环形缓冲涉及的代码与数据全部经过_go32_dpmi_lock_data/_go32_dpmi_lock_code锁页,确保中断发生时不会因缺页或 DPMI 重入而崩溃(SDL_dosaudio_sb.c 第 339-356 行)。

对游戏主循环的建议

  • 游戏帧率 ≥ 22 fps 时,音频无需额外处理即可保持流畅;
  • 帧率低于 20 fps 时,在游戏循环中段加入一次SDL_Delay(0)让音频线程有机会补充环形缓冲即可;
  • 音频线程的WaitDevice在缓冲满时也会DOS_Yield(),让主线程继续运行——协作式调度的典型体现(第 168-187 行)。

其他约束

  • DMA 缓冲必须分配在常规内存中且不能跨越 64 KB 物理页边界(8237 DMA 控制器的硬件限制)。DOS_AllocateDMAMemory采用"分配 2 倍大小、必要时使用后半段"的标准手法解决(见 src/core/dos/SDL_dos.c);
  • 缓冲大小上限为 32 KB(buffer_size > 32 * 1024报错),因为 DMA 缓冲要翻倍后塞进 64 KB 段内(SDL_dosaudio_sb.c 第 226-228 行);
  • 音频录制未实现,驱动只提供播放(OnlyHasDefaultPlaybackDevice = true)。

输入:键盘、鼠标与游戏杆

  • 键盘:由 IRQ1 驱动,支持完整扩展扫描码(0xE0 前缀,即方向键、小键盘区等);
  • 鼠标:通过 INT 33h 鼠标驱动,使用 mickeys(相对位移脉冲)提供相对移动;
  • 游戏杆:通过直接读游戏端口 0x201(gameport)实现,带软件校准。必须在初始化时已连接,没有热插拔检测;文档特别提示:运行中断开游戏杆可能导致性能下降。

已知限制

  • 无动态加载:不支持共享库/动态加载(没有SDL_LoadObject)。文档提到 DXE(DJGPP 的动态可执行文件格式)支持"未来可能加入"。因此,依赖插件机制的 DOS 应用需要以静态链接方式编译进所有功能。

从源码进一步探索

以下文件可以帮助你深入理解 DOS 移植的每个细节:

  • 构建与工具链:build-scripts/i586-pc-msdosdjgpp.cmake
  • 协作式调度器:src/core/dos/SDL_dos_scheduler.c、src/core/dos/SDL_dos_scheduler.h
  • 内存/中断辅助:src/core/dos/SDL_dos.c、src/core/dos/SDL_dos.h
  • 视频驱动:src/video/dos/SDL_dosvideo.c、src/video/dos/SDL_dosmodes.c、src/video/dos/SDL_dosframebuffer.c
  • 音频驱动:src/audio/dos/SDL_dosaudio_sb.c、src/audio/dos/SDL_dosaudio_sb.h
  • Hint 定义:include/SDL3/SDL_hints.h(SDL_HINT_DOS_ALLOW_DIRECT_FRAMEBUFFER,第 793-808 行)

小结

SDL3 的 DOS 移植是一套精心适配 16 位时代硬件约束的完整实现:用 fat DS 打通保护模式与硬件地址空间,用 setjmp/longjmp 构建协作式线程,用 IRQ + 环形缓冲让 Sound Blaster 在低帧率下依然流畅,用dosmemput/LFB 双路径适配不同档次的 VESA 显卡。对开发者而言,最重要的三条实践准则是:自定义main()时手动启用 fat DS、主循环定期调用SDL_PumpEvents/SDL_Delay让出 CPU、需要极致帧吞吐时在首次SDL_GetWindowSurface()之前设置直接帧缓冲 hint

【免费下载链接】SDLSimple DirectMedia Layer项目地址: https://gitcode.com/GitHub_Trending/sd/SDL

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 16:10:26

2026年实测可用Docker国内镜像源清单与配置指南

前一阵把主力开发机迁移到新系统&#xff0c;Docker 装好后第一件事就是拉mysql:8.0。结果老收藏夹里那几个“国内镜像源地址”接连败下阵来——不是 TLS handshake timeout&#xff0c;就是直接 403。去论坛翻了十几个“最新可用 Docker 国内镜像源”的帖子&#xff0c;一大半…

作者头像 李华
网站建设 2026/9/14 16:08:43

Vue 3 实战进阶:10个避免踩坑的高效技巧与性能优化指南

写这篇文章之前&#xff0c;我先说明一个背景。最近团队在招前端&#xff0c;面试里问了不下二十个候选人关于 Vue 3 组合式 API 的用法&#xff0c;发现一个很有意思的现象&#xff1a;很多人能背出ref、reactive、computed的定义&#xff0c;但一落到真实业务场景&#xff0c…

作者头像 李华
网站建设 2026/9/14 16:08:40

RustFox:10MB、启动<1秒的极简API调试工具原理与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 16:08:17

NRBO-SVM多变量时序预测在Matlab中的实现与优化

1. 项目背景与核心价值 在工业预测和科研分析领域&#xff0c;多变量时序预测一直是个硬骨头。传统单一模型往往顾此失彼——要么抓不住长期趋势&#xff0c;要么忽略短期波动&#xff0c;更别提超参数调优这个老大难问题。最近在Matlab圈子里火起来的NRBO-SVM组合拳&#xff0…

作者头像 李华
网站建设 2026/9/14 16:07:30

ClickHouse v23.10.6.60-stable 更新详解:22 项 Bug 修复与源码定位

ClickHouse v23.10.6.60-stable 更新详解&#xff1a;22 项 Bug 修复与源码定位 【免费下载链接】ClickHouse ClickHouse is a real-time analytics database management system 项目地址: https://gitcode.com/GitHub_Trending/cli/ClickHouse 本文基于当前仓库中的版本…

作者头像 李华