Stay Hungry, Stay Foolish

FFmpeg + DirectX12 简易视频播放器随记(其一)

尽管我不知道为什么会有人想直接使用 DirectX 来做视频播放器,甚至还是 DirectX 12 这样一个麻烦的东西,总之这坨代码已经被我糊出了个雏形。虽然功能十分简陋,没有音频输出,视频的播放速度也不对,但总之能放了。为了避免经验快速从我的大脑流失,我还是决定记录一下我的整个实现思路历程。 把大象塞进冰箱 很显然,我们要做一个视频播放器,至少我们需要先有一个视频,然后把它播放出来。那么很好,就像把大象塞进冰箱一样,我们现在只需要两步,第一步是把视频的画面内容从文件里面提取出来,第二步是把视频的画面内容画到屏幕上。 好吧,我承认我在第一步上偷懒了。起初我是想用 Nvidia 的 NVDecoder 来干这活的,但由于我是倒着来干这活的——先实现第二步,再实现第一步,所以实现到后面就犯了懒,拿 FFmpeg 凑合过了。毕竟,为了验证第一步是否能够正确解码出画面内容,那必须先实现第二步的内容画面绘制才行。当然,一个更好的借口是,利用 FFmpeg 可以更加方便地实现对于多种软硬件编解码器实现的支持,不过这些重点并不在我这篇随记的内容范围内。避免篇幅过长,在这篇中我就仅对 DirectX 12 的渲染过程作记录,而 FFmpeg 的解码 API 调用则放到后续篇章中。 DirectX12 渲染一张图片 Win32 中文显示 在 Windows 上编程,总有几个比较烦人的点。其中字符编码就是一个比较大的问题。由于我想减少对于 Visual Studio 这一庞然大物的依赖,我选择使用 VSCode 和 CMake 来构建我的程序。由于对字符编码是在不甚了解,在这个项目中,折腾中文的显示也花费了我好一番功夫。最后,我沉淀出来的解决方案分为两个关键步骤。 首先,众所周知,Win32 大多数 API 都有两个版本,一个是后缀为 A 的 ANSI 版本,一个是后缀为 W 的宽字符版本。为了能让 Windows 正确处理源文件中的中文字符,我们需要使用 Win32 API 的宽字符版本。Win32 API 对具有两个版本的 API 都写了对应的宏,即不带后缀的版本,会根据当前是否定义了 UNICODE 宏来确定使用 A 版本或 W 版本。所以,为了让 Windows 的宏自动选择 W 版本的 Win32 API,在引入 Windows 的各项头文件前需要手动定义 UNICODE 宏。当然,如果你全部手动使用 W 版本的 Win32 API,倒也可以省去这一步。具体而言,头文件引入长得像下面这个样子: ...

July 20, 2024 · 7 min · H1RA

Debian 12 软路由配置随记

最近自己拉了一条联通的千兆宽带,并成功地找官方客服要到了公网 IPv4。为了有效利用这个公网 IP,同时避免重演之前在房东的光猫上的血泪史,我决定将拨号等各项网络功能全部交由软路由实现,光猫仅保留广电转换功能。这篇文章用于记录我的配置过程,防止以后在软路由配置上重复踩坑,也为需要的人提供参考。 准备工作 在开始配置软路由之前,需要安装一系列工具,并对系统的一些参数进行修改。我使用的工具包括: pppoeconf,用于宽带拨号 wide-dhcpv6-client,用于获取 IPv6 PD 前缀并配置到接口 dnsmasq,用于为 LAN 提供 DNS 服务、DHCP 服务和 IPv6 路由通告服务 nftables,用于提供 IPv4 NAT 和防火墙功能 以上三个工具均可通过apt直接安装。 同时,为了保证系统能够正确获取 IPv6 地址和转发数据包,还需要向/etc/sysctl.conf中添加以下内容: net.ipv4.ip_forward=1 net.ipv6.conf.all.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0 net.ipv6.conf.all.forwarding=1 net.ipv6.conf.default.forwarding=1 net.ipv6.conf.all.accept_ra=2 net.ipv6.conf.default.accept_ra=2 其中,第一条用于启用对 IPv4 数据包的转发,后面六条分别对应启用 IPv6 功能、启用对 IPv6 数据包的转发和接受 IPv6 的路由器通告。带default的配置用于系统对接口的默认设置,如果未对其进行修改,可能导致新添加的接口与 all 的配置不一致,从而产生问题。(在我的场景中,拨号后新增的ppp0接口的accept_ra项为1,无法正确从 ISP 的 RA 通告中获取并设置 IPv6 地址,导致其只有公网 IPv4 地址和 FE80 打头的 IPv6 link local 地址,没有 GUA,即公网 IPv6 地址。) 配置网络接口 在进行后续工作前,首先需要正确配置软路由 LAN 口和 WAN 口的地址。方便起见,我直接使用 Debian 12 自带的networking服务进行网络配置,不使用NetworkManager等管理器。具体配置如下,其中enp1s0为 LAN 口,LAN 的 IPv4 网段为10.20.0.0/24,enp4s0为 WAN 口,连接光猫,IPv4 地址与光猫处于同一网段,方便后续对光猫进行操作。 ...

July 5, 2024 · 3 min · H1RA

从 TTY 到 PTY,程序的输入、输出与控制

TTY 设备在 Linux 系统中的应用比较广泛,但很少有人去深度探讨 TTY 设备在整个 Linux 系统交互中起到的关键性作用。本文整合了几篇较为经典的文章对 TTY 设备整体工作原理、机制及应用的探讨,希望能够为读者带来帮助。 从功能性的角度看 其实,从功能性的角度来看,Linux 的 TTY 设备就是受操作系统管理的双向管道,用户通过外设(一般是键盘)输入的数据会进入到 TTY 设备文件,经过终端驱动,传递给操作系统进行处理,操作系统输出到 TTY 设备的数据则会被输出到外设(一般是屏幕),让用户能够看见,本质上就是一个从外设到操作系统的双向管道。 PTY 和 TTY 在功能上是一致的,不过它可以被用户进程创建。用户进程可以通过 Linux 系统提供的接口创建一个 PTS Pair,一个 PTS Pair 分为 Master 设备和 Slave 设备两种,输出到 Master 设备的数据会被传递到 Slave 设备的输入,而输出到 Slave 设备的数据会被传递到 Master 设备的输入。如果我们将显示器绑定到 Master 设备的输出上面,再将键盘绑定到 Master 设备的输入上面,那么我们就能够通过 Master 设备间接操作绑定到 Slave 设备的进程了。因为 PTY 和 TTY 本身并没有太大的差别,为避免过于啰嗦,在下面我只针对 PTY 进行讲述。 一个例子:SSH SSH 的设计中也使用了 PTY 设备,我们在通过 SSH 登入主机进入 Shell 后,可以通过 tty 命令去获取当前的 Shell 进程运行在哪个 PTY 上面。 上面tty命令给出的结果/dev/pts/dev是当前运行的 Shell 绑定的 PTY Slave 设备,而对应的 Master 设备文件描述符只有通过 /dev/ptmx创建 PTY Pair 的进程持有,都指向/dev/ptmx,可以通过sudo lsof /dev/ptmx查看。我们在使用 SSH 的时候,本地的 Terminal Emulator 将输入的字符发送到 SSH 进程,SSH 进程收到字符以后通过网络传输给服务器上面的 SSHD 进程,SSHD 进程将字符写入到 Master 设备,Shell 就能够从 Slave 设备中读取到字符。同理,Shell 的输出写入到 Slave 设备,被 SSHD 从 Master 设备处读取,通过网络传递给 SSH 客户端,最终 SSH 客户端将字符显示到屏幕上(当然,一般我们都是在 Terminal Emulator 里面运行的 SSH 进程,所以本地的 SSH 进程也是要通过 TTY 设备和键盘以及屏幕通讯的,这里图方便就没有画出来,大概样子和图 1 差不多)。 ...

March 3, 2023 · 3 min · H1RA

Netlink 笔记

在用户空间的程序有的时候可能需要和内核进行消息交互,内核需要提供一种机制来满足用户空间程序的这一需求。虽然系统调用确实提供了一个用户空间程序和内核的交互机制,但是系统调用在一些轻量级的交互上未免显得过于重量级。为此,Linux 内核在用于请求特定信息的经典系统调用以外,还提供了另外的几种用户空间和内核的通信接口,分别是procfs,sysctl,sysfs,ioctl以及 netlink 套接字。procfs和sysfs都是虚拟文件系统,这一部分暂且不提,sysctl对应的是/proc/sys文件,实际上是一个内核变量,这一部分也不在这里详谈,ioctl则是在设备控制上做的工作,细说起来估计也能单开一篇文章了。本文主要还是对 Netlink 的机制做一个简单的概括与总结。 Netlink 是什么?为什么是 Netlink? 按书里的描述总结来看,Netlink 是一个允许内核内部以及内核与用户空间的程序相互进行通信的消息传输机制,是对标准套接字实现的拓展。在 Netlink 诞生之前,一般使用 procfs、sysfs 和 ioctl 来进行内核和用户空间程序之间的数据交互。相比其他机制,Netlink 具有一些更明显的优势: 任何一方都不需要轮询,像 procfs 和 sysfs 这类利用文件进行信息传递的机制,用户空间程序需要不断轮询文件来及时获取信息的更新。 系统调用和 ioctl 也能够从用户空间向内核传递信息,但是相比 Netlink 更难以实现。另外,使用 Netlink 不会与其他的内核模块产生冲突,但模块和系统调用显然配合得不是很好。(这里是《深入 Linux 内核架构》的原话,不过我还不是很理解) 内核可以直接向用户层发送信息,而不需要用户空间程序事先向内核请求。这一点使用文件也能做到,但是系统调用和 ioctl 显然不能够做到。 用户只需要使用标准的套接字和内核进行交互。 内核不仅支持单播消息,也支持多播,并且 Netlink 的工作方式是异步的。Netlink(3) 和 Netlink(7) 两个手册页提供了 Netlink 机制的文档,其中 netlink(3) 描述了内核中用于操作、访问、创建 Netlink 数据报的宏。手册页 netlink(7) 包含了有关 Netlink 套接字的一般性信息,并给出了这里其的数据结构的文档。另外,/proc/net/netlink里面包含了关于当前活动的 Netlink 连接的一些信息。 Netlink 是怎么工作的? 从创建说起 从用户空间程序来看,Netlink 和普通的 BSD 套接字长得很像。在创建 Netlink 套接字的时候,也需要用socket()函数创建一个 socket 描述符,再为其分配一个地址。创建 Netlink 套接字的时候,需要指定 family 为AF_NETLINK,而 type 既可以指定为SOCK_RAW,也可以是SOCK_DGRAM,最终创建的都是一个netlink_sock对象。至于 protocol,根据该 Netlink 套接字的用途,有不同的选择,具体可以看《深入 Linux 内核架构》第 650 页或者是 Netlink 的手册。 ...

December 12, 2022 · 2 min · H1RA

《Linux 内核设计的艺术》第一部分笔记

前言 最近粗略拜读了朋友推荐的《Linux 内核设计的艺术》这本书。不过相比于 Linux 内核设计的艺术,我从这本书中获得的最大启发其实是 Linux 启动的流程。尽管这本书中的 Linux 版本比较古早,和现行的 Linux 发行版的启动方式大相径庭,不过也从一个更底层的角度向我展示了整个操作系统的引导流程,不管是 I/O 子系统还是中断子系统,也让我对内核的工作方式和存在结构有了全新的认识。遗憾的是,我的这些问题,如果早点读到《操作系统真象还原》这本书的话,可能能得到更好的解答(当然,《操作系统真象还原》这本书讲的并不是 Linux,而是 x86 体系上的通用操作系统概念)。下文的讲述将可能引用《操作系统真象还原》这本书的内容,简洁起见(省得打字),下面将《操作系统真象还原》简称为《真象》,而《Linux 内核设计的艺术》简称为《艺术》。 第一行指令 在先前的计算机组成课中,我了解到了指令在系统中是如何流转的,但这种就是一个比较底层的视角,我还是没能计算机组成与操作系统这两门课很好地结合到一起。这本书与其他讲述 Linux 内核的书籍不同之处在于,不是一上来就基于代码模块对整个 Linux 内核进行分块的剖析,而是从按下开机键上电到 Linux 启动完毕的流程娓娓道来,这正是我所欠缺的,将操作系统与计算机组成统一起来的视角。 指令从何而来? 计算机的心脏,CPU,从上电那一刻起就在不停地搏动。那么第一下搏动从何而来呢?每条指令需要在 CPU 上执行,才能产生效果,因此触发第一条指令执行的,必定不是某一条指令,而是只有不需要前置条件,上电即可触发的硬件。在 Intel 8086 CPU 架构中,上电之后 CPU 的 CS:IP 会被指向地址0xFFFF0,这个地址下会有一条指令,跳转到 BIOS 程序的真正入口点。那么问题又来了,这第一条指令又是从哪里来的呢? 这个点在《真象》里面是这么解释的。在 I/O 子系统中,我们曾经学过一种 CPU 的 I/O 方式,即内存映射 IO,内存映射 I/O 通过将外设于一块特别的内存区域绑定到一块,这样指令可以直接通过访问那块区域的内存来读写外设上面的数据。在 CPU 能够访问到的地址空间里面,我们可以将一部分地址空间用于 I/O,剩下的内存地址空间再绑定到物理内存上面。因为 BIOS 是计算机上面执行的第一行指令,所以 BIOS 必定是由硬件加载的,而这里的硬件,就是 ROM。ROM 也是一块内存,不过这块内存是非易失性的,这块内存的地址空间被硬件映射到内存地址空间0xF0000~0xFFFFF处,因此,当我们访问这块地址的时候,实际上我们是在访问 ROM 中的内容,就不会有加载的问题。 BIOS 的工作是什么? 当 BIOS 的第一行指令开始上 CPU 执行后,BIOS 的指令就会被源源不断地读进 CPU 并执行了。现在 CPU 正处于一个叫做实模式的状态下。为什么会是这个模式,以及实模式和保护模式的区别将在后续的内容中讲到,这里只需要注意,实模式下的访存方式,和我们在计算机组成里面学到的访存机制基本一致,是直接将 CS:IP 的值组装成地址之后通过地址总线访问,不需要额外的转换工作。Intel 8086 留给实模式的地址线只有 20 根,所以只能够访问到 1MB 的内存。随着 BIOS 的指令执行,它会检查所有外设的状态信息,如果没有检测到错误,那么下一步,就要开始准备中断向量表和中断服务程序,为下面的操作系统引导铺路。 ...

December 10, 2022 · 2 min · H1RA