
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 移植采用 DJGPPGNU 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 需要下载并解压 DJGPP 工具链将包含i586-pc-msdosdjgpp-gcc的目录加入PATH环境变量保证系统已安装 CMake。工具链文件会按顺序查找两个编译器i586-pc-msdosdjgpp-gcc或i386-pc-msdosdjgpp-gccC 编译器对应-g找不到则直接报错终止配置。从源码看build-scripts/i586-pc-msdosdjgpp.cmakei586变体优先。构建命令cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILEbuild-scripts/i586-pc-msdosdjgpp.cmake -DCMAKE_BUILD_TYPERelease 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 hostDOS Protected Mode Interface 宿主负责提供实模式/保护模式切换、内存锁页等服务。移植文档明确要求将CWSDPMI.EXE放在可执行文件旁边。CWSDPMI是 DJGPP 自带的 DPMI 服务程序。在 DOSBox 中运行只需dosbox myapp.exeDOSBox 内置了 DPMI 支持通常无需额外安装 CWSDPMI但保持二者兼容无误仍是好习惯。系统要求组件最低要求CPUi386 或更高RAM4 MB视频VGA256 色模式 13h音频Sound BlasterDPMICWSDPMI.exe 或兼容的 DPMI host更高的分辨率640×480 及以上需要VESA VBE 1.2 兼容的显卡。这是硬性门槛视频驱动在初始化时通过 VBE 中断0x10功能0x4F00查询 VESA 信息探测显卡能力不满足要求就返回错误。从 src/video/dos/SDL_dosvideo.c 可以看到驱动创建时首先调用DOSVESA_SupportsVESA()失败则直接返回 NULLSDL 会以清晰错误提示退出。内存模型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 DSSDL 在 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_PumpEvents或SDL_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_JoinThread、DOS_BlockCurrentThread等待信号量与DOS_WakeThread构成完整的线程同步原语第 257-302 行。视频VGA 13h 与 VESA VBE支持的显示模式视频驱动SDLDOSVESA支持两类模式VGA 模式 13h320×200×256任何 VGA 显卡都支持作为兜底模式。由于多数 VESA BIOS 不把 13h 列入模式列表代码中用哨兵值VGA_MODE_13H_SENTINEL标识并回退到0xA000段见 src/video/dos/SDL_dosmodes.cVESA 扩展模式VBE 1.2支持 640×480 及更高分辨率同时支持线性帧缓冲VBE 2.0 LFB和分段银行banked两种访问方式。只有软件渲染没有 GPU 渲染器。所有视频模式实际上都是全屏的——驱动在创建窗口时通过SDL_GetClosestFullscreenDisplayMode选择最接近请求尺寸的可用模式src/video/dos/SDL_dosvideo.cdevice_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 模式则回退到标准文本模式0x03BIOS 中断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_FRAMEBUFFERSDL_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 位立体声有符号S16LE5–44.1 kHzSB ProDSP 3.x8 位立体声无符号U8最高 22050 Hz立体声SB 2.0 / 1.xDSP 2.x / 1.x8 位单声道无符号U8硬件配置完全自动从BLASTER环境变量解析。解析器IsSoundBlasterPresent识别A基端口十六进制、IIRQ、D8 位 DMA 通道、H16 位高 DMA 通道等关键字忽略M混音芯片端口、PMPU-401、T设备类型等无关项src/audio/dos/SDL_dosaudio_sb.c。缺失或错误的变量会返回明确错误。典型配置形如SET BLASTERA220 I5 D1 H5 T6检测流程还会通过 DSP 复位握手和版本查询命令0xE1确认端口上确实是 Sound Blaster而非其他设备。环形缓冲架构约 45 ms 的余量音频路径采用IRQ 驱动的环形缓冲这是移植文档强调的核心设计SDL 音频线程协作式通过完整的 SDL 音频管线含分配、互斥将混音结果写入环形缓冲Sound Blaster IRQ 处理器在中断上下文中只做一件事从环形缓冲拷贝一个 chunk 到 DMA 半缓冲纯memcpy 端口 I/O不碰 DPMI、不分配、不加锁、不用 FPU环形缓冲容量为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 KBbuffer_size 32 * 1024报错因为 DMA 缓冲要翻倍后塞进 64 KB 段内SDL_dosaudio_sb.c 第 226-228 行音频录制未实现驱动只提供播放OnlyHasDefaultPlaybackDevice true。输入键盘、鼠标与游戏杆键盘由 IRQ1 驱动支持完整扩展扫描码0xE0 前缀即方向键、小键盘区等鼠标通过 INT 33h 鼠标驱动使用 mickeys相对位移脉冲提供相对移动游戏杆通过直接读游戏端口 0x201gameport实现带软件校准。必须在初始化时已连接没有热插拔检测文档特别提示运行中断开游戏杆可能导致性能下降。已知限制无动态加载不支持共享库/动态加载没有SDL_LoadObject。文档提到 DXEDJGPP 的动态可执行文件格式支持未来可能加入。因此依赖插件机制的 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.hHint 定义include/SDL3/SDL_hints.hSDL_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),仅供参考