问题

计算机底层是如何访问显卡的?

回答
计算机底层访问显卡是一个相当复杂的过程,涉及到多个层次的协作,从操作系统到显卡驱动,再到显卡硬件本身。下面我将尽量详细地阐述这个过程:

核心概念:

在深入细节之前,理解几个关键概念非常重要:

CPU (中央处理器): 负责执行程序指令,包括计算和数据处理。
GPU (图形处理器): 显卡的核心,专门用于处理图形渲染、并行计算等任务。
内存 (RAM): 计算机的临时存储空间,CPU 和 GPU 都需要访问内存来存储数据和指令。
显存 (VRAM): 显卡上的专用内存,用于存储纹理、帧缓冲区、着色器代码等图形相关数据。
PCIe (Peripheral Component Interconnect Express): 一种高速串行总线,用于连接 CPU 和各种外围设备,包括显卡。
显卡驱动 (Graphics Driver): 一种特殊的软件,充当操作系统和显卡硬件之间的桥梁,负责将操作系统发出的图形指令翻译成显卡能够理解和执行的低级命令。
图形 API (Application Programming Interface): 如 DirectX (Windows) 和 OpenGL/Vulkan (跨平台)。这些 API 为应用程序提供了一套标准化的接口,用于与显卡进行交互,而无需直接操作硬件。

访问流程概览:

当应用程序需要渲染图形时,其底层访问显卡的过程大致如下:

1. 应用程序通过图形 API 发出指令。
2. 图形 API 将应用程序的抽象指令转换为更底层的图形命令。
3. 操作系统接收这些命令,并将其传递给显卡驱动。
4. 显卡驱动将图形命令翻译成特定于显卡硬件的低级指令。
5. 显卡驱动通过 PCIe 总线将这些指令和数据发送到显卡。
6. 显卡硬件(GPU)接收并执行这些指令,进行图形渲染或计算。
7. 渲染结果(像素数据)被写入显存中的帧缓冲区。
8. 显卡的视频输出控制器读取帧缓冲区中的数据,并通过显示接口(如 HDMI, DisplayPort)发送到显示器。

详细分解每个阶段:

第一阶段:应用程序层面

程序逻辑: 应用程序(如游戏、图形设计软件)的 CPU 部分负责准备渲染数据,例如:
加载模型顶点数据。
准备纹理数据。
定义着色器(Shader)代码(用于控制顶点和像素的颜色和光照)。
设置相机位置、光源等场景信息。
图形 API 调用: 应用程序并不直接操作硬件,而是通过图形 API 来完成这些任务。例如,在 C++ 中使用 DirectX:
```c++
// 示例:绘制一个三角形
ID3D11DeviceContext pDeviceContext; // 显卡设备上下文
ID3D11Buffer pVertexBuffer; // 顶点缓冲区
ID3D11Buffer pIndexBuffer; // 索引缓冲区
ID3D11VertexShader pVertexShader; // 顶点着色器
ID3D11PixelShader pPixelShader; // 像素着色器

// ... 设置好顶点数据、索引数据、着色器 ...

// 绑定着色器和输入布局
pDeviceContext>VSSetShader(pVertexShader, 0, 0);
pDeviceContext>PSSetShader(pPixelShader, 0, 0);

// 绑定顶点缓冲区和索引缓冲区
UINT stride = sizeof(Vertex);
UINT offset = 0;
pDeviceContext>IASetVertexBuffers(0, 1, &pVertexBuffer, &stride, &offset);
pDeviceContext>IASetIndexBuffer(pIndexBuffer, DXGI_FORMAT_R32_UINT, 0);

// 设置图元类型(例如,三角形列表)
pDeviceContext>IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

// 发起绘制调用
pDeviceContext>DrawIndexed(numIndices, 0, 0);
```
这里 `DrawIndexed` 是一个高层次的指令,告诉显卡要绘制什么。

第二阶段:图形 API 和操作系统内核

API 实现: 图形 API 本身也是软件,运行在用户空间(User Mode)。它负责将应用程序发出的高层次指令,例如“绘制一个三角形”、“设置纹理”,转换为更具体的操作,例如:
将顶点数据上传到显存。
编译和加载着色器。
设置渲染状态(如颜色混合、深度测试)。
上下文切换: 当图形 API 调用需要与显卡硬件交互时,通常会涉及从用户空间切换到内核空间(Kernel Mode)。这是因为直接访问硬件的权限通常在内核空间。
Direct Rendering Path / Indirect Rendering Path:
Direct Rendering Path (DRM): 在 Linux 等系统中,图形 API(通过 Mesa 等开源驱动)可能直接与内核的 DRM (Direct Rendering Manager) 子系统交互,后者更直接地管理硬件。
Indirect Rendering Path: 在 Windows 中,图形 API (DirectX) 的实现通常在用户空间的 DLL (例如 `d3d11.dll`) 中,它会将请求传递给操作系统内核的服务。
I/O Request Packets (IRPs) Windows: 在 Windows 中,用户模式的服务(如 DirectX)会创建一个 IRP(I/O Request Packet),包含要执行的操作和相关数据,然后通过系统调用将其发送到内核中的图形驱动程序。

第三阶段:显卡驱动程序(Kernel Mode Driver & User Mode Driver)

显卡驱动是访问显卡的枢纽,它通常包含两部分:

User Mode Driver (UMD):
负责处理应用程序发出的绝大多数图形 API 调用。
将 API 调用转换为驱动程序内部的命令列表(Command Buffer)。
进行一些优化,例如将多个小命令合并成一个大命令。
管理用户空间的资源(如纹理对象、缓冲区对象)。
在必要时,将这些命令列表提交给内核模式驱动。
Kernel Mode Driver (KMD):
是驱动程序的核心部分,运行在操作系统内核空间。
负责直接与显卡硬件进行通信。
接收来自 UMD 的命令列表。
将这些命令列表转换成硬件可以理解的低级命令(通常是特定的指令集,如 NVIDIA 的 NV50、AMD 的 GCN 指令集)。
通过 PCIe 总线将这些命令发送到显卡。
管理显卡硬件资源,例如分配显存、配置 GPU 工作参数。
处理硬件中断,例如当 GPU 完成一个任务时,会产生一个中断通知驱动程序。
提供对硬件功能的访问接口,例如创建和销毁 GPU 对象。

第四阶段:PCIe 总线通信

DMA (Direct Memory Access): 显卡驱动程序(KMD)会将待处理的命令和数据(如顶点数据、纹理)通过 DMA 的方式直接从系统内存传输到显卡的显存中。这使得 GPU 可以独立于 CPU 工作,效率更高。
PCIe Transaction: 命令和控制信息通过 PCIe 协议发送。这些信息会路由到显卡上的各种控制器:
PCIe Endpoint: 显卡上的 PCIe 控制器负责接收和发送 PCIe 数据包。
Command Processor: 接收来自驱动程序的命令,并将其解码、调度给 GPU 的各个处理单元。
Memory Controller: 管理显卡的显存(VRAM),负责数据的读写。

第五阶段:显卡硬件(GPU)

GPU 架构: GPU 内部包含许多高度并行化的处理单元,如:
流处理器 (Stream Processors / CUDA Cores / Shading Units): 负责执行顶点着色器、像素着色器等计算任务。
纹理单元 (Texture Units): 负责从显存中读取纹理数据并进行过滤。
光栅化器 (Rasterizer): 将几何图形(三角形)转换为屏幕上的像素网格。
ROP (Render Output Unit): 负责将像素数据写入帧缓冲区,并执行颜色混合、深度测试等操作。
几何着色器 (Geometry Shader)、曲面细分着色器 (Tessellation Shader) 等: 根据需要执行更高级的几何处理。
执行流程:
1. 命令解码和调度: GPU 的命令处理器接收来自 PCIe 的命令,并将其分发给相应的硬件单元。
2. 顶点处理: 顶点数据经过顶点着色器处理,计算出每个顶点在屏幕上的位置。
3. 光栅化: 三角形被光栅化成像素。
4. 像素处理: 每个像素的颜色由像素着色器计算。
5. ROP 操作: 最终的像素颜色被写入显存中的帧缓冲区,同时可能进行深度测试(确保物体在正确的位置)。
显存访问: GPU 直接访问显存(VRAM)来读取纹理、存储顶点/索引数据、写入渲染结果(帧缓冲区)。CPU 通过显卡驱动和 PCIe 向 GPU 发出指令,告诉 GPU 要去显存的哪个位置读取什么数据,以及将结果写到哪里。

第六阶段:显示输出

帧缓冲区 (Framebuffer): GPU 完成渲染后,最终的图像数据存储在显存的一个区域,称为帧缓冲区。
显示控制器 (Display Controller): 显卡上的另一个关键组件。它负责从帧缓冲区读取像素数据,并按照显示器所需的格式(分辨率、刷新率、色彩深度)通过显示接口(HDMI, DisplayPort, DVI, VGA)发送到显示器。
同步: 显示控制器会与显示器进行同步,以确保画面稳定流畅。

总结:

整个过程是一个分层、解耦的体系:

应用程序 表达“要做什么”(如绘制一个三角形)。
图形 API 翻译成一套标准化的指令。
操作系统 负责管理进程和硬件访问权限。
显卡驱动(UMD+KMD)将标准指令翻译成硬件可执行的低级命令,并管理硬件资源。
PCIe 总线 提供高速的数据传输通道。
GPU 硬件 执行具体的图形计算和渲染任务。
显示控制器 将最终图像输出到屏幕。

这种分层结构的好处是:

抽象化: 应用程序开发者不必关心不同显卡硬件的细节,只需遵循图形 API 规范即可。
兼容性: 操作系统和驱动程序负责处理硬件差异。
性能: 现代图形系统通过硬件加速、DMA 等技术来优化性能。

理解这个过程有助于我们更深入地理解为什么需要显卡驱动,为什么游戏需要优化,以及为什么不同的图形 API 会有不同的性能表现。

网友意见

user avatar

在这里回答一下第一个,第二个,第四个和第五个问题。

在回答这个问题之前,必须要有一些限定。因为显卡是有很多种,显卡所在平台也很多种,不能一概而论。我的回答都是基于Intel x86平台下的Intel自家的GEN显示核心单元(也就是市面上的HD 4000什么的)。操作系统大多数以Linux为例。

>>> Q1. 显卡驱动是怎么控制显卡的, 就是说, 使用那些指令控制显卡, 通过端口么?

目前的显卡驱动,不是单纯的一个独立的驱动模块,而是几个驱动模块的集合。用户态和内核态驱动都有。以Linux桌面系统为例,按照模块划分,内核驱动有drm/i915模块, 用户驱动包括libdrm, Xorg的DDX和DIX,3D的LibGL, Video的Libva等等,各个用户态驱动可能相互依赖,相互协作,作用各不相同。限于篇幅无法一一介绍。如果按照功能划分的话,大概分成5大类,display, 2D, 3D, video, 以及General Purpose Computing 通用计算。Display是关于如何显示内容,比如分辨率啊,刷新率啊,多屏显示啊。2D现在用的很少了,基本就是画点画线加速,快速内存拷贝(也就是一种DMA)。3D就复杂了,基本现在2D的事儿也用3D干。3D涉及很多计算机图形学的知识,我的短板,我就不多说了。Video是指硬件加速的视频编解码。通用计算就是对于OpenCL,OpenCV,CUDA这些框架的支持。

回到问题,驱动如何控制显卡。

首先,操作硬件的动作是敏感动作,一般只有内核才有权限。个别情况会由用户态操作,但是也是通过内核建立寄存器映射才行。

理解驱动程序最重要的一句话是,寄存器是软件控制硬件的唯一途径。所以你问如何控制显卡,答案就是靠读写显卡提供的寄存器。

通过什么读写呢?据我所知的目前的显卡驱动,基本没有用低效的端口IO的方式读写。现在都是通过MMIO把寄存器映射的内核地址空间,然后用内存访问指令(也就是一般的C语言赋值语句)来访问。具体可以参考”内核内存映射,MMIO“的相关资料。

>>>Q2.2. DirectX 或 OpenGL 或 CUDA 或 OpenCL 怎么找到显卡驱动, 显卡驱动是不是要为他们提供接口的实现, 如果是, 那么DirectX和OpenGL和CUDA和OpenCL需要显卡驱动提供的接口都是什么, 这个文档在哪能下载到? 如果不是, 那么DirectX, OpenGL, CL, CUDA是怎么控制显卡的?

这个问题我仅仅针对OpenGL和OpenCL在Linux上的实现尝试回答一下。

a.关于如何找到驱动?首先这里我们要明确一下驱动程序是什么,对于OpenGL来说,有个用户态的库叫做LibGL.so,这个就是OpenGL的用户态驱动(也可以称之为库,但是一定会另外再依赖一个硬件相关的动态库,这个就是更狭义的驱动),直接对应用程序提供API。同样,OpenCL,也有一个LibCL.so.。这些so文件都依赖下层更底层的用户态驱动作为支持(在Linux下,显卡相关的驱动,一般是一个通用层驱动.so文件提供API,然后下面接一个平台相关的.so文件提供对应的硬件支持。比如LibVA.so提供视频加速的API,i965_video_drv.so是他的后端,提供Intel平台对应libva的硬件加速的实现)。 下面给你一张大图:

如图可见,最上层的用户态驱动向下依赖很多设备相关的驱动,最后回到Libdrm这层,这一层是内核和用户态的临界。一般在这里,想用显卡的程序会open一个/dev/dri/card0的设备节点,这个节点是由显卡内核驱动创建的。当然这个open的动作不是由应用程序直接进行的,通常会使用一些富足函数,比如drmOpenByName, drmOpenByBusID. 在此之前还会有一些查询的操作,查询板卡的名称或者Bus ID。然后调用对应的辅助函数打开设备节点。打开之后,他就可以根据DRI的规范来使用显卡的功能。我说的这一切都是有规范的,在Linux里叫DRI(Direct Rendering Infrastructure)。

所有这些图片文档都可以

Direct Rendering Infrastructure

和 freedesktop上的页面DRI wiki找到

DRI Wiki

显卡驱动的结构很复杂,这里有设计原因也有历史原因。

b.关于接口的定义,源代码都可以在我上面提供的链接里找到。这一套是规范,有协议的。

c.OpenGL, OpenCL或者LibVA之类的需要显卡提供点阵运算,通用计算,或者编解码服务的驱动程序,一般都是通过两种途径操作显卡。第一个是使用DRM提供的ioctl机制,就是系统调用。这类操作一般包括申请释放显存对象,映射显存对象,执行GPU指令等等。另一种是用户态驱动把用户的API语意翻译成为一组GPU指令,然后在内核驱动的帮助下(就是第一种的执行GPU指令的ioctl)把指令下达给GPU做运算。具体细节就不多说了,这些可以通过阅读源代码获得。

>>>Q4. 显卡 ( 或其他设备 ) 可以访问内存么? 内存地址映射的原理是什么, 为什么 B8000H 到 C7FFFH 是显存的地址, 向这个地址空间写入数据后, 是直接通过总线写入显存了么, 还是依然写在内存中, 显卡到内存中读取, 如果直接写到显存了, 会出现延时和等待么?

a..可以访问内存。如果访问不了,那显示的东西是从哪儿来的呢?你在硬盘的一部A片,总不能自己放到显卡里解码渲染吧?

b.显卡访问内存,3种主要方式。

第一种,就是framebuffer。CPU搞一块内存名叫Framebuffer,里面放上要显示的东西,显卡有个部件叫DIsplay Controller会扫描那块内存,然后把内容显示到屏幕上。至于具体如何配置成功的,Long story, 这里不细说了。

第二种,DMA。DMA懂吧?就是硬件设备直接从内存取数据,当然需要软件先配置,这就是graphics driver的活儿。在显卡驱动里,DMA还有个专用的名字叫Blit。

第三种,内存共享。Intel的平台,显存和内存本质都是主存。区别是CPU用的需要MMU映射,GPU用的需要GPU的MMU叫做GTT映射。所以共享内存的方法很简单,把同一个物理页既填到MMU页表里,也填到GTT页表里。具体细节和原理,依照每个人的基础不同,需要看的文档不同。。。

c.为什么是那个固定地址?这个地址学名叫做Aperture空间,就是为了吧显存映射到一个段连续的物理空间。为什么要映射,就是为了显卡可以连续访问一段地址。因为内存是分页的,但是硬件经常需要连续的页。其实还有一个更重要的原因是为了解决叫做tiling的关于图形内存存储形势和不同内存不一致的问题(这个太专业了对于一般人来说)。

这地址的起始地址是平台相关,PC平台一般由固件(BIOS之流)统筹规划总线地址空间之后为显卡特别划分一块。地址区间的大小一般也可以在固件里指定或者配置。

另外,还有一类地址也是高位固定划分的称为stolen memory,这个是x86平台都有的,就是窃取一块物理内存专门为最基本的图形输出使用,比如终端字符显示,framebuffer。起始地址也是固件决定,大小有默认值也可以配置。

d. 刚才说了,Intel的显存内存一回事儿。至于独立显卡有独立显存的平台来回答你这个问题是这样的:任何访存都是通过总线的,直接写也是通过总线写,拷贝也是通过总线拷贝;有时候需要先写入临时内存再拷贝一遍到目标区域,原因很多种;写操作都是通过PCI总线都有延迟,写谁都有。总线就是各个设备共享的资源,需要仲裁之类的机制,肯定有时候要等一下。

>>>Q5. 以上这些知识从哪些书籍上可以获得?

Intel Graphics for Linux*

, 从这里看起吧少年。这类过于专业的知识,不建议在一般经验交流的平台求助,很难得到准确的答案。你这类问题,需要的就是准确答案。不然会把本来就不容易理解的问题变得更复杂。

user avatar

楼上说了很多了,补充点具体的,以前 DOS下做游戏,操作系统除了磁盘和文件管理外基本不管事情,所有游戏都是直接操作显卡和声卡的,用不了什么驱动。

虽然没有驱动,但是硬件标准还是放在那里,VGA, SVGA, VESA, VESA2.0 之类的硬件标准,最起码,你只做320x200x256c的游戏,或者 ModeX 下 320x240x256c 的游戏的话,需要用到VGA和部分 SVGA标准,而要做真彩高彩,更高分辨率的游戏的话,就必须掌握 VESA的各项规范了。

翻几段以前写的代码演示下:

例子1: 初始化 VGA/VESA 显示模式

基本是参考 VGA的编程手册来做:

       INT 10,0 - Set Video Mode  AH = 00  AL = 00  40x25 B/W text (CGA,EGA,MCGA,VGA)     = 01  40x25 16 color text (CGA,EGA,MCGA,VGA)     = 02  80x25 16 shades of gray text (CGA,EGA,MCGA,VGA)     = 03  80x25 16 color text (CGA,EGA,MCGA,VGA)     = 04  320x200 4 color graphics (CGA,EGA,MCGA,VGA)     = 05  320x200 4 color graphics (CGA,EGA,MCGA,VGA)     = 06  640x200 B/W graphics (CGA,EGA,MCGA,VGA)     = 07  80x25 Monochrome text (MDA,HERC,EGA,VGA)     = 08  160x200 16 color graphics (PCjr)     = 09  320x200 16 color graphics (PCjr)     = 0A  640x200 4 color graphics (PCjr)     = 0B  Reserved (EGA BIOS function 11)     = 0C  Reserved (EGA BIOS function 11)     = 0D  320x200 16 color graphics (EGA,VGA)     = 0E  640x200 16 color graphics (EGA,VGA)     = 0F  640x350 Monochrome graphics (EGA,VGA)     = 10  640x350 16 color graphics (EGA or VGA with 128K)    640x350 4 color graphics (64K EGA)     = 11  640x480 B/W graphics (MCGA,VGA)     = 12  640x480 16 color graphics (VGA)     = 13  320x200 256 color graphics (MCGA,VGA)     = 8x  EGA, MCGA or VGA ignore bit 7, see below     = 9x  EGA, MCGA or VGA ignore bit 7, see below   - if AL bit 7=1, prevents EGA,MCGA & VGA from clearing display  - function updates byte at 40:49;  bit 7 of byte 40:87    (EGA/VGA Display Data Area) is set to the value of AL bit 7     

转换成代码的话,类似这样:

       // enter standard graphic mode int display_enter_graph(int mode) {   short hr = 0;  union REGS r;  memset(&r, 0, sizeof(r));  if (mode < 0x100) {    r.w.ax = (short)mode;   int386(0x10, &r, &r);   r.h.ah = 0xf;   int386(0x10, &r, &r);   if (r.h.al != mode) hr = -1;  }   else {    r.w.ax = 0x4f02;   r.w.bx = (short)mode;   int386(0x10, &r, &r);   if (r.w.ax != 0x004f) hr = -1;  }  return hr; }      

基本就是通过中断指令,调用 INT 0x10的 0x00 方法,初始化VGA显示模式,如果模式号大于256,那么说明是一个 VESA显示模式,调用 VESA的中断函数来进行。

例子2: 画点

如果你初始化成功了 320 x 200 x 256 c 模式(INT 0x10, AX=0x13),那么画点就是象显存地址 0xA00000L 里面写一个字节(8位色彩深度):

我们使用 DOSBOX (DOS开发调试神器)来演示,启动 DOSBOX以后,运行

       debug     

然后写两条进入图形模式的指令:

       mov ax, 13 ; 设置  ah=0(0号函数上面有说明), al=0x13(0x13模式,320x200) int 10 ; 调用显卡中断 int 20 ; DOS命令:退出程序     

输入空行后退出编辑模式,然后使用 'g' 命令运行刚才的这个小程序:

可以看到,显示模式初始化成功了,现在你已经进入了 320x200x256c的显示模式,大量的 DOS游戏是使用这个模式开发出来的(仙剑奇侠传,轩辕剑1/2,C&C)。

接下来我们编辑显存,使用 e命令,进行内存编辑(0xa00000L),注意这里我们还是实模式,显存需要拆分成段地址:0xa000,和偏移0000 来访问:

       -e a000:0000     


用了e 命令,写入了一连串字节,值都是“4”,点击放大上面的窗口,可以看到左上角已经被我写了几个点了,默认调色板下颜色 “4” 是红色。


接着在 A000:0300处(坐标第3行第128列)写入更多颜色,这次更明显些,注意上面中间:

放大些:

这次写入了更多颜色,而且是在第三行中间部分,没挨着dosbox的窗口边缘,看起来更清晰了,是吧?

好了,有了上面的试验后,我们可以写代码了,大概类似这样:

       void putpixel(int x, int y, unsigned char color) {  static unsigned char far *videobuf = (unsigned char far*)0xa0000000;  if (x >= 0 && y >= 0 && x < 320 && y < 200) {   videobuf[y * 320 + x] = color;  } }       

上面代码可用 TurboC++2.0, Borland C++ 3.1, TurboC2 来编译,当然,当年这么写是不行的,硬件慢的要死,各种 trick当然是能上则上,正确的写法是:

       void putpixel(int x, int y, unsigned char color) {  static unsigned char far *videobuf = (unsigned char far*)0xa0000000;  if (((unsigned)x) < 320 && ((unsigned)y) < 200) {   videobuf[(y << 6) + (unsigned)(y << 8) + x] = color;  } }      

优化了两处,范围判断改用 unsigned以后,少了两次 >= 0的判断,同时乘法变成了移位和加法,旧式 cpu计算乘法总是那么慢。有了画点,写一个画线画圆画矩形就容易了,再返照写一个图块拷贝(BitBlt)也很容易,有了这些,应该够开发一个传统游戏。

旧游戏里绘制一般都是在系统内存中进行的,在内存中开辟一块模仿显存的区域,进行画点画线,贴图,绘制好以后,一个memcpy,直接拷贝到显存。但实模式下线性地址只有64KB,可用总内存只有差不多640KB,要存储大量的图元是很困难的,稍不注意就内存不够了,因此 DOS下开发游戏,最好都是上 Watcom C++ 或者 Djgpp(dos下的gcc导出)。

Watcom C++ 可以在dos下实现4g内存访问,现在可以下载 OpenWatcom 来编译,我不太喜欢 Djgpp,编译太慢,加上一大堆著名游戏都是 Watcom C++写成的,导致我更加鄙视 Djgpp 因此我之前主要是在 Watcom C++下开发,除去上面的画点外,后面翻到的代码片段基本都是 Watcom C++的。

例子3:设置调色板

看到这里,也许你不禁要发问:除了直接写显存外,好像各种初始化工作都是调用 BIOS 里预先设置好的 INT 10h中断来完成啊,这 INT 10h 又是具体怎么操作显卡的呢?

其实 INT10h 也可以画点(AH=0C, AL=颜色, BH=0, DX=纵坐标,CX=横坐标),BIOS的 INT 10h中画点实现其实也是直接写显存,但是执行的很慢,基本没人这么用,都是直接写显存的,操作显卡除了访问显存外,有些功能还需要访问端口来实现。

接下来以初始化调色盘为例,256色同屏每个点只有0-255的调色板索引,具体显示什么颜色需要查找一个:256 x 3 = 768 字节的调色板(每个索引3个字节:RGB)。设置一个颜色的调色盘需要先向 0x03c8端口写入颜色编号,接着在 0x03c9端口依次写入R,G,B三个分量的具体数值,具体指令为:

       mov edx, 0x03c7 mov al, color out dx, al inc dx mov al, R out dx, al mov al, G out dx, al mov al, B out dx, al     

我们可以使用 Watcom C++ 的 outp 函数来实现 out指令调用:

       void display_set_palette(unsigned char color, char r, char g, char b) {   short port = 0x03c8;  outp(port, color);   port++;   outp(port, r);   outp(port, g);   outp(port, b); }      

这个例子用到的是 OUT指令写端口,x86架构下 OUT 可以向特定端口写入数据,端口你可以理解为和数据总线并立的另外一个 I/O 控制总线,通过北桥南桥映射到各个硬件的 I/O 数据引脚,x86 下通过端口可以方便的操作显卡,软盘,硬盘,8254计时器,键盘缓存,DMA 控制器等周边硬件,后面我们还会频繁使用。

而其他硬件下,并没有端口这样一个控制总线存在,那 GBA / NDS 里没有端口这样的存在,他们是用何种方法访问各种外设呢?答案是内存地址映射,GBA / NDS 下有一段低端内存地址被映射给了 I/O RAM,通过直接读写这些地址,就可以跟x86的 out/in 指令一样控制各硬件,完成:显示模式设置,显示对象,图层,时钟,DMA,手柄,音乐等控制。

好了,现可以通过端口读写调色板了,我们调用一下上面这个函数:display_set_palette (4, 0, 63, 0); 就可以把上面通过直写显存画在左上角的一串红色点(颜色4)改变成绿色而不用重新写显存了,注意,vga的调色盘是 6位的,RGB的最大亮度为63。

传统256色的游戏中需要正确设置调色板才能让图像看起来正确,否则就是花的,使用调色板还有很多用法,比如游戏中常见的 fade in / fade out 效果,就是定时,每次把所有颜色的调色板读取出来,R,G,B各-1,然后保存回去,就淡出了,还有一些类似调色板流动等用法,可以很方便的制作波浪流动(图片不变,只改变调色板),传统游戏中用它来控制海水效果,不需要每次重绘,比如老游戏中天上闪烁的星星,大部分都是调色板变动一下。

例子4:初始化 ModeX

传统标准显卡有三种显示模式:VGA模式(模式号低于256),VESA模式(模式号高于256,提供更高解析度,真彩高彩等显示模式,以及线性地址访问)此外还有著名的 ModeX,Michael Abrash 提出 ModeX 以后,由于对比其他标准模式具备更好的色彩填充性能,因此在不少游戏中也得到了广泛使用,但是其初始化非常的 trick,主体代码为:

       outpw(0x3C4, 0x0100);                     /* synchronous reset */ outp(0x3D4, 0x11);                        /* enable crtc regs 0-7 */ outp(0x3D5, inp(0x3D5) & 0x7F); outpw(0x3C4, 0x0604);                     /* disable chain-4 */  for (reg=mode->regs; reg->port; reg++) {  /* set the VGA registers */  if (reg->port == 0x3C0) {   inp(0x3DA);   outp(0x3C0, reg->index | 0x20);   outp(0x3C0, reg->value);  }   else if (reg->port == 0x3C2) {   outp(reg->port, reg->value);  }    else {   outp(reg->port, reg->index);    outp(reg->port + 1, reg->value);  } }  if (mode->hrs) {  outp(0x3D4, 0x11);  outp(0x3D5, inp(0x3D5) & 0x7F);  outp(0x3D4, 0x04);  outp(0x3D5, inp(0x3D5) + mode->hrs);  outp(0x3D4, 0x11);  outp(0x3D5, inp(0x3D5) | 0x80); }  if (mode->shift) {  outp(0x3CE, 0x05);   outp(0x3CF, (inp(0x3CF) & 0x60) | 0x40);  inp(0x3DA);           outp(0x3C0, 0x30);  outp(0x3C0, inp(0x3C1) | 0x40);  for (c=0; c<16; c++) {   outp(0x3C0, c);   outp(0x3C0, c);  }    outp(0x3C0, 0x20); } if (mode->repeat) {  outp(0x3D4, 0x09);  outp(0x3D5, (inp(0x3D5) & 0x60) | mode->repeat); } outp(0x3D4, 0x13);                       /* set scanline length */ outp(0x3D5, width / 8); outpw(0x3C4, 0x0300);                    /* restart sequencer */      

是不是有点天书的感觉?直接控制硬件就是这么琐碎,前面初始化显示模式都是用 int 10h中断完成的,其实 int 10h中断本身也是通过各种写端口来重置垂直扫描频率,水平扫描频率,显存映射方式,开始地址等达到具体设置某一个分辨率的目的,也就是说其实你可以绕开int 10h用自己的方式设置出一个新的显示模式来,ModeX 就是这样初始化的。

不得不说一句今天有驱动程序真幸福,费了我九牛二虎之力才初始化成功的 ModeX,今天一个函数调用就完成了,大家也发现了使用 int 10h中断,调用 BIOS 里面的预设程序控制显卡,只是初级用法,现在基本只用在 grub 等操作系统加载程序上了,进入了操作系统后,就再也不会调用 int 10h,而是赤裸裸的直接和显卡打交到。

例子5:显存分段映射

早期显卡的显存只能按 64KB 大小分成若干个 bank 来映射到特定的物理地址,也就是说你使用 640 x 480 x 32bits 的显示模式时,全屏幕总共需要 1200KB 的显存来表示屏幕上面的每一个点,而由于显存每次只能分段映射一个 64KB 大小的 bank,所以每次写屏前都要把对应位置的显存先映射过来才能写,我们使用下面代码来切换 bank:

       int display_vesa_switch(int window, int bank) {  union REGS r;  r.x.eax = 0x4f05;  r.x.ebx = window;  r.x.edx = bank;  int386(0x10, &r, &r);  return 0; }      

注意这里还有个窗口概念,一个窗口可以映射一个bank,大部分显卡只有一个窗口,则只能同时映射一段64KB的显存给cpu访问,而有的显卡有两个窗口(一个读,一个写)。

这个是标准做法,访问中断很慢,在绘制过程中频繁的访问中断是要命的,故 Trident 系列的显卡提供直接访问端口的方法来切换页面(Trident只有一个窗口,同时只能映射一个64KB的bank):

       int display_trident_switch(unsigned char bank) {  outp(0x3c4, 0x0e);  outp(0x3c5, bank ^ 0x2);  return 0; }      

而如果你使用支持 VESA2.0 标准的显卡,在保护模式下,VESA2的接口提供了一系列函数入口供你调用,你可以直接在 Watcom C++ 下面调用这些函数完成页面切换,比调用中断的开销小多了。

听起来十分美妙,但是你想导出这些函数的入口地址来调用的话,你将需要:

1. 分配一块物理内存并锁定地址。

2. 调用vesa中断,向这个物理地址写入这些函数的代码。

3. 为该物理内存分配一个 selector 段地址,才能读取这些代码并拷贝到 Watcom C++默认段。

4. 按照入口表,初始化 Watcom C++ 里面的函数指针,并释放物理内存。

5. 然后你才可以开心的调用这些函数。

这个函数表,可以理解成就是 VESA 2.0 的一个初级阶段的驱动程序了。

简单一个页面切换,上面就提到了三种做法,你可以选择最保险也是性能最差的中断调用,也可以根据显卡支持选择写端口或者直接调用导出函数。

好在游戏基本都是二级缓存来绘制的,主要的绘制工作在系统内存的二级缓存里面完成,最后只需要在 memcpy搬运到显存显示出来的时候,再去设置页面映射,然后整个 bank一次性拷贝,然后再切换到下一个 bank,这样“设置页面映射” 这个操作的调用次数就会比较少了。

这就是早年访问多于 64KB 显存的基本方法,多用在解析度超过 320x200 的模式中,如果你继续使用流行的 320x200 显示模式,你将不需要考虑这个事情,因为全屏幕只需要 62.5 KB的显存,没有切换 BANK 的需要。

但是早期缺乏统一的编程接口,今天这个显卡扩充一点功能,明天那个品牌又多两个效果,弄得你疲于奔命,因此 Windows 以后,这些工作都统一交给显卡驱动来完成了。

例子6:线性显存映射

由于分辨率越来越高,越来越多的软件用到了 640x480x256以上的显示模式,传统的 bank 映射方式已经显得越来越落后了,因此90年代中期的显卡纷纷开始支持 VESA 2.0 中的 “线性地址映射”,通过一些列初始化工作,将显示模式设置为 “线性地址”,这样在保护模式下,你就可以一整块的访问连续显存而用不着切换 bank了。

这是一种十分简单高效的方式,只要你的游戏用 Watcom C++ / Djgpp 开发,跑在保护模式下,这可以说是最美妙方方式了,可惜,当年并不是所有显卡都支持这样的方式,碰到不兼容的显卡你还得绕回去使用 bank 切换。

所以为了在 640x480x256c 下面正确绘制图形,一共有四种显存访问方式(bank切换3种+线性地址映射),应用程序写的好的话,需要把访问显存统一封装一下,并提供类似这样的接口:

       //--------------------------------------------------------------------- // Framebuffer Access //---------------------------------------------------------------------  // copy rect from memory to video frame buffer void display_bits_set(int sx, int sy, const void *src, long pitch,   int x, int y, int w, int h);  // get rect from frame buffer to memory void display_bits_get(int sx, int sy, void *dst, long pitch,   int x, int y, int w, int h);  // write row to video frame buffer void display_row_write(int x, int y, const void *buffer, int npixels);  // read row from video frame buffer void display_row_read(int x, int y, void *buffer, int npixels);      

背后则需要判断显卡的特性,普通显卡使用兼容性最好的的方式,而好点的显卡使用更快速的方式,并为上层提供统一的访问 framebuffer 的接口,由于早年的 C++ 编译器优化十分有限,这部分基本都是上千行的汇编代码直接实现,于是你又得捧起486、奔腾优化手册来,慢慢调试一点点计算 u,v 流水线的开销并安排好指令让它们最大程度并行执行。

一直到了 DirectX 时代,整个事情才简单了很多,微软一句话,所有 DirectX 兼容显卡必须支持线性地址映射,因此 DirectX 下面 Lock 一个 Surface 后可以毫无拘束连续访问显存,这样一个简单的操作,对比前面的实现,简直是一件十分幸福的事情。

例子7:DMA控制器访问

CPU对大部分基础周边设备,都是通过写内存或端口I/O来控制的,比如前面很多显卡控制,比如操作 8254为CPU提供时钟中,比如操作 8237 DMA控制器来实现直接内存访问。

比如使用 DMA CHANNEL 1 传送数据到周边设备的代码类似:

       outp(0x0a, 0x05);    // 禁用 dma channel 1 outp(0x0b, 0x45);    // 设置读取模式 outp(0x0c, 0);       // 准备设置地址 outp(0x02, addr & 0xff);         // 物理地址第一个字节 outp(0x02, (addr >> 8) & 0xff);  // 物理地址第二个字节 outp(0x83, (addr >> 16) & 0xff); // 物理地址第三个字节 outp(0x0c, 0);       // 结束设置地址 outp(0x03, (size - 1) & 0xff);   // 设置长度 outp(0x03, ((size - 1) >> 8) & 0xff); // 设置长度高位 outp(0x0a, 0x01);    // 开始传送 channel 1      

传统 DMA控制器有 8个通道式对应不同周边设备,且只能访问低端16MB地址空间,保护模下的程序需要在16MB的空间内分配连续的物理内存页面,并映射到当前的进程默认地址空间中,传输 DMA时需要将物理地址(非虚拟地址)传送给 DMA 控制器。

具体端口代表的意思,一般是需要查看硬件手册,并按说明调用。早期部分显卡已经开始支持 DMA传送数据,同时也支持显存传送,PC中 DMA传送数据未必有 CPU快,但是却能和 CPU保持异步,解放 CPU 去做更多的事情。

不同的 DMA 通道对应不同的外设,比如硬盘,声卡,显卡,传输是否完成可以通过中断的方式或者 CPU 查询 DMA控制器端口得知,DMA控制器会用类似锁存器的方式保证你分两次读出的高低字节表示同一个变量。

标准 PC/DMA 无法实现 内存-> 内存的 DMA 异步传输,用起来没 ARM 的 DMA 那么爽,比如 GBA / NDS 下有异步 memcpy 函数 DmaArrayCopy,原理是在特定的物理地址(被映射成 I/O RAM部分)写入数据: *((u32*)0x40000D4) 写入源地址,*((u32*)0x40000D8) 写入目标地址,*((u32*)0x40000DC) 写入长度后,异步拷贝就开始了,十分飘逸。

连续通过 DMA传输数据的话,一般需要开辟双缓存,一块传输着,一块准备着,交替进行。

----------------------

Windows后,不能让我读写端口曾让我郁闷了很久,但时代变迁,看着今天各种规范的 API 接口,统一的硬件规范,对比以前繁琐的实现,突然有种淡淡的幸福。

先写这么多吧,主要是上补充下其他答案,提供一点具体的感受,顺便也和大家一起怀旧一下。

--

类似的话题

  • 回答
    计算机底层访问显卡是一个相当复杂的过程,涉及到多个层次的协作,从操作系统到显卡驱动,再到显卡硬件本身。下面我将尽量详细地阐述这个过程:核心概念:在深入细节之前,理解几个关键概念非常重要: CPU (中央处理器): 负责执行程序指令,包括计算和数据处理。 GPU (图形处理器): 显卡的核心,.............
  • 回答
    关于唐朝底层五口之家一天消耗粮食在6到8斤,以及由此推断“唐朝人都饿肚子”的结论,这确实是一个值得深入探讨的问题。作为一名对历史充满好奇的普通人,我来试着用更贴近生活、更详细的方式梳理一下这个问题,看看这个结论站不站得住脚。首先,我们来拆解一下“6到8斤粮食”这个数字。这个数字是怎么算出来的?科普视.............
  • 回答
    要说“懂计算机底层”,这事儿可就有点意思了。它不是像学个新菜谱,照着步骤来就能做出同样味道的菜。这更像是在一片荒芜的土地上,你要把地犁好,种子播下,还得知道怎么浇水、怎么施肥,最后才能长出东西来。所以,“懂”的程度,其实是个动态的、不断深入的过程。但我可以给你描绘一个大概的图景,让你知道大概要往哪个.............
  • 回答
    接到这个情况,我的第一反应是:别急着拆!框架结构房屋,墙体底端是混凝土打的,这可不是个小事,它很可能不是你想象中那么简单的“墙面”。这涉及到房屋的结构安全,所以你问“还应该继续吗?”,我的答案是:在充分了解情况并寻求专业意见之前,不应该贸然继续。让我详细说说为什么:1. 框架结构房屋的本质:首先,要.............
  • 回答
    这个问题很复杂,不能简单地说城市底层生活一定比农村好,或者反过来。这其中涉及的因素太多,而且每个人的感受和经历都不一样。先说说农村生活,尤其是你提到的“国家专项计划”这类政策。这确实是国家为了促进教育公平,给农村贫困地区的孩子们提供的一些倾斜政策。比如,有一些大学会专门为这些地区拿出一定的招生名额,.............
  • 回答
    计算机是否可以模拟现实世界的一切,是一个涉及科学、哲学、数学和工程学的复杂问题。以下是对此问题的详细分析: 一、计算机模拟的基本原理计算机模拟的核心是通过数学模型和算法,将现实世界的物理规律、化学反应、生物过程等抽象为可计算的规则,然后在计算机上运行这些规则,从而重现现实中的现象或系统。例如: 天气.............
  • 回答
    作为一名计算机专业的应届本科毕业生,你的薪资范围会受到很多因素的影响,因此无法给出一个绝对精确的数字。但是,我可以为你提供一个详细的薪资分析和影响因素的解读,帮助你更好地理解和预估。一、 大致薪资范围 (一线城市为例,不含年终奖、期权等)首先,要明确一点,不同城市、不同公司、不同岗位、不同技术栈的薪.............
  • 回答
    计算机视觉是否已经进入瓶颈期是一个非常复杂的问题,没有一个简单的“是”或“否”的答案。更准确的说法是,计算机视觉领域正处于一个转型期,在某些方面取得了巨大的进步,但在其他方面,尤其是在实现真正人类水平的理解和泛化能力方面,依然面临着严峻的挑战,可以说是遇到了“瓶颈”或“高原期”。为了详细阐述这个问题.............
  • 回答
    计算机视觉中的目标跟踪是一个至关重要的研究领域,旨在在视频序列中持续地定位和识别一个或多个目标。随着深度学习的兴起,目标跟踪算法取得了显著的进展。以下是一些计算机视觉中经典的目标跟踪算法,我将尽量详细地介绍它们的核心思想、特点和发展历程: 早期经典算法(基于手工特征和滤波)在深度学习普及之前,目标跟.............
  • 回答
    计算机理解图像的过程,是一个将我们人类视觉世界转化为数字信息并进行分析和解释的复杂旅程。它不像人类那样通过眼睛和大脑的生物机制来感知,而是依赖于一系列精密的算法和数学模型。我们可以将其分解为几个关键阶段:第一阶段:图像的数字化(Pixelization) 模拟信号到数字信号的转换: 现实世界的图.............
  • 回答
    “计算机再过几年会没落?” 这是一个非常有趣且具有挑战性的问题。我的回答是:不太可能,但计算机的概念和形式会发生深刻的演变,以至于我们现在理解的“计算机”可能会被超越,甚至被边缘化,但其底层驱动和核心功能将以新的形态继续存在并蓬勃发展。要详细解释这一点,我们需要从几个层面来剖析“计算机的没落”以及“.............
  • 回答
    这个问题很有趣,并且触及了计算机科学教育和职业规划的许多重要方面。简单来说,计算机本科生花大量时间写编译器或操作系统,绝对不是不务正业,反而非常有价值,但也要看具体的学习目标和资源投入。下面我们来详细分析一下:为什么写编译器和操作系统“不是不务正业”?1. 深度理解计算机底层原理的基础: .............
  • 回答
    是的,计算机在德州扑克比赛中不仅可以战胜人类,而且在某些特定情况下,已经能够以压倒性的优势战胜最顶尖的人类玩家。这并非易事,而是多年来人工智能(AI)研究,特别是博弈论和机器学习领域深入探索的成果。为了详细说明这一点,我们可以从以下几个方面来解读:1. 德州扑克本身的复杂性德州扑克之所以成为AI研究.............
  • 回答
    这个问题问得非常好,它涉及到计算机内部处理文本的底层原理和不同编码的优劣势。简单来说,计算机不是“不直接使用 UTF8 进行存储”,而是更准确地说,计算机在内部更倾向于使用一种统一的、能够表示所有字符的抽象表示,然后根据需要将其转换为不同的字节序列表示(编码),而 UTF8 就是最常用的一种字节序列.............
  • 回答
    计算机大牛们,你们好!我是一个正在努力学习 C++ 的初学者,最近在阅读 C++ 相关书籍时遇到了一些困惑,想和大家交流一下。首先,我想请教一个普遍的问题:各位大牛在看 C++ 有关书籍的时候,是不是都能做到一遍就看懂呢?我总觉得自己有些笨,看一些地方需要反复阅读好几遍才能勉强理解,甚至有些概念还是.............
  • 回答
    当我们将计算机、蜘蛛网和蜂巢放在一起审视时,确实能发现它们在“生物造物”这个标签下,似乎有着某种程度的共性,都展现了精巧的设计和复杂的功能。然而,深入探究它们的起源、构成、运作机制以及“目的性”,我们就能清晰地看到它们之间存在的、可以说是“本质上”的区别。首先,让我们从它们的来源说起。蜘蛛网是生物造.............
  • 回答
    计算机考研选学校,性价比高是个很实在的考量点。毕竟,读研不仅是为了知识和学历,更是为了未来的职业发展和个人提升,咱们得把每分钱花在刀刃上。所谓的“性价比”,我理解得更像是:投入的精力、金钱(学费、生活费),能换来一个相对不错的研究平台、学术氛围,并且对未来的就业或者继续深造有实质性的帮助。咱们不谈那.............
  • 回答
    在计算机发展史上,许多关于技术、硬件和软件的谣言流传甚广,它们往往源于误解、技术术语的混淆或营销宣传。以下是一些经典的计算机世界谣言及其详细解析: 1. “电脑会自己写诗” 谣言背景:早期AI研究中,一些程序被设计为生成文本(如诗歌、故事)。例如,1950年艾伦·图灵的“图灵测试”中,计算机被要求模.............
  • 回答
    计算机视觉(Computer Vision, CV)是人工智能的重要分支,其核心目标是让计算机理解和处理图像或视频中的信息。CV的算法种类繁多,根据任务目标和应用场景的不同,可以分为多个层次和类别。以下是对主要算法类型的详细分类及其特点的全面解析: 一、图像处理基础算法1. 图像增强与变换 灰.............
  • 回答
    计算机视觉(CV)方向今年的招聘情况可以用 “机遇与挑战并存,部分领域趋于饱和,但新兴和细分领域仍有需求” 来概括。 简单地说,不能简单地说人才过剩,但市场竞争确实比前几年激烈,对求职者的技能和经验要求更高。为了更详细地说明情况,我们可以从以下几个方面来分析:1. 整体招聘需求与市场变化: AI.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有