当前位置: 首页 > news >正文

游戏引擎学习第246天:将 Worker 上下文移到主线程创建

回顾并为今天的工作做准备

关于GPU驱动bug的问题,目前本地机器上没有复现。如果有问题,昨天的测试就应该已经暴露出来了。当前演示的是游戏的过场动画,运行正常,使用的是硬件渲染。
之前使用软件渲染时没有遇到太多问题,这是因为软件渲染不会直接使用GPU,自然不会触发复杂的兼容性问题。而一旦开始使用GPU,就容易遇到兼容性问题。这是因为与GPU通信的协议本身就非常复杂和混乱,而且不同GPU硬件及其驱动程序每年都会有很大变化,因此一旦使用GPU,出现兼容性问题的概率就非常高。
如果不涉及GPU,大部分情况下兼容性问题很少,但现代游戏为了在图形性能上具有竞争力,基本上必须大量使用GPU,因为GPU上提供了更多高效的图形功能。

根据论坛上的反馈,在NVIDIA显卡上,如果使用多线程下载纹理,会出现问题。调查后发现,问题出在OpenGL上下文(OpenGL context)的创建方式上。
具体来说,如果所有需要的OpenGL上下文都在主线程创建好,然后再分发给各个子线程使用,那么一切运行正常;但是如果在各个子线程中分别创建OpenGL上下文,就会出问题。

目前还不清楚具体原因,已经将源代码发送给了NVIDIA,希望能得到官方解释。但即使没有明确解释,也能推测可能是因为OpenGL在内部使用了线程局部存储(Thread Local Storage),主线程调用OpenGL时初始化了某些关键数据,而其他线程由于没有这些初始化数据,导致无法正常创建上下文。不过这只是猜测,具体机制还不得而知。

可以确定的是,直接在子线程上新建OpenGL上下文是不可靠的,只能在主线程上统一创建好后再分发使用。这也是PC平台开发的常态:硬件环境极其复杂,遇到的兼容性问题很多时候没办法彻底搞清楚,只能通过观察和实验总结经验并找到解决办法。

这次的问题幸运地相对简单。解决方案是:

  • 在主线程上,预先为每个需要的子线程创建好OpenGL上下文。
  • 启动子线程时,将已经创建好的OpenGL上下文传递给它们,由子线程直接使用预建的上下文。

接下来回顾之前的实现方式。
初始化OpenGL的代码集中在 Win32InitOpenGL 函数中,然后在子线程中调用 Win32CreateOpenGLContextForWorkerThread 来为子线程单独创建OpenGL上下文。
问题就在于,Win32CreateOpenGLContextForWorkerThread 是在子线程里被调用的,而这正是导致兼容性问题的原因。

因此,需要修改流程:

  • 在启动子线程之前,在主线程一次性调用创建所有所需的OpenGL上下文。
  • 然后把这些上下文传递给子线程,子线程不再自己创建上下文,只负责绑定和使用传入的上下文。

这样可以确保代码在NVIDIA驱动和其他厂商的驱动上都能正常运行,兼容性问题得到解决。


win32_game.h:将 platform_work_queue 从 win32_game.cpp 中移入

接下来要做的事情很直接,并不复杂。
主要需要改动的是,在启动线程时,除了传递平台工作队列(PlatformWorkQueue),还需要给线程传递更完整、更多信息的数据结构。

目前的实现中,我们过于依赖平台工作队列,线程启动时仅仅接收了一个PlatformWorkQueue对象。可以在代码中看到,线程启动函数ThreadProc,接收了一个PlatformWorkQueue指针。这个队列是多个线程共享的,它只负责调度需要执行的任务。

因为这个队列是共享的,所以无法通过它来为每个线程分别传递独立的数据,比如每个线程需要绑定的OpenGL上下文。每个线程需要使用自己独立的数据,这就要求启动线程时传递一个专用的、包含所有必要信息的结构体。

实际上,在代码中早就有了这样一个结构体,只是之前遗留下来了,没有充分利用。这个结构体叫做Win32ThreadStartup。之前在做多线程处理时,曾经考虑过需要它,所以这个结构体已经存在。

Win32ThreadStartup本来就是为了在创建线程时传递更丰富的信息而设计的。
现在只需要扩展使用它,除了传递线程应该使用的PlatformWorkQueue之外,还可以附加每个线程独立需要的数据,比如对应的OpenGL渲染上下文(OpenGL RC,Render Context)。

因此,修改思路总结如下:

  1. 将原本直接传递PlatformWorkQueue的地方,改成传递Win32ThreadStartup结构体。
  2. Win32ThreadStartup中,除了包含工作队列,还包含该线程需要使用的OpenGL上下文。
  3. 启动线程时,每个线程根据自己收到的Win32ThreadStartup信息,拿到对应的工作队列和OpenGL上下文,进行正常工作。

这就是全部需要做的事情了,整体改动并不大,但能解决线程之间独立数据传递的问题,同时也能解决之前提到的由于子线程自己创建OpenGL上下文导致的兼容性问题。
在这里插入图片描述

之前做过了

在这里插入图片描述

在这里插入图片描述

win32_game.cpp:在 ThreadProc 中初始化线程和队列,并更改为测试以确保我们拥有 OpenGLRC


接下来需要继续完成线程启动流程的调整。
具体要做的是:在线程启动函数ThreadProc中,不再直接使用PlatformWorkQueue作为参数,而是使用更完整的Win32ThreadStartup结构体作为启动参数。这个结构体不仅包含了原先需要的工作队列,还可以附带每个线程独有的数据,比如OpenGL上下文。

具体步骤如下:

  • 线程启动时,接收参数并将其强制转换为Win32ThreadStartup指针。
  • 原本直接从参数中提取工作队列的代码,需要调整成从Win32ThreadStartup结构体中提取。
  • 这样虽然仍然是传递了工作队列,但它现在被包含在了一个更完整的启动数据包里,因为后续还需要更多线程专属的信息,比如OpenGL的RC(Render Context)。

接着,要处理线程内的OpenGL上下文绑定:

  • 删除原来在工作队列里标记需要OpenGL的逻辑。
  • 在线程启动时,判断传入的启动参数中是否包含了OpenGL的RC。
  • 如果有RC,需要调用wglMakeCurrent,将当前线程的OpenGL上下文切换成对应的RC。

注意,为了能调用wglMakeCurrent,还需要一个有效的DC(Device Context,设备上下文)。虽然这些线程本身不会直接在窗口上渲染,只是提交纹理等后台工作,但Windows要求必须有一个有效的DC传进去。

这里参考了之前查询到的资料,发现处理多线程纹理下载时,通常做法是继续传递一个现有的、有效的DC,而不是自己去新建一个。因此计划是,将原来已有的DC传递给线程,用于绑定OpenGL上下文。

所以,需要在Win32ThreadStartup结构体中加入DC的字段,比如叫做OpenGLDC,并在线程启动时一起传递进去。线程内部就可以通过这个DC和RC正确调用wglMakeCurrent,完成OpenGL上下文的切换。

总结下来,目前已经完成的内容是:

  1. 修改线程启动时的参数,从PlatformWorkQueue切换成Win32ThreadStartup
  2. 在线程启动函数中,从Win32ThreadStartup中提取工作队列,同时提取OpenGL的RC和DC。
  3. 在子线程中,根据RC和DC正确设置OpenGL上下文。
  4. 移除原先在工作队列上处理OpenGL标记的逻辑。

接下来只需要在创建线程时,正确地构建这个包含所有信息的Win32ThreadStartup数据包,并传给每个线程就可以了。


win32_game.cpp:让 Win32MakeQueue 接受 win32_thread_startup


接下来需要处理线程启动调用的地方。
在原本的代码中,调用ThreadProc时,只简单地传递了一个工作队列参数。现在由于需要传递更复杂的线程初始化信息,包括OpenGL上下文和DC,所以必须改为传递Win32ThreadStartup结构体实例。

具体调整方式如下:

  • 在创建线程时,不再只传递单纯的工作队列,而是传递一个Win32ThreadStartup实例。
  • 因为每个线程需要持有自己的启动信息,所以要提前创建一个Win32ThreadStartup数组,每个元素对应一个线程。
  • 启动线程时,将对应的Win32ThreadStartup实例传给线程。

这样一来,每个线程就能通过自己的启动参数拿到独立的OpenGL上下文和必要的设备上下文(DC)。

为了简化调用方的操作,增加了一个小优化:

  • 在线程创建函数内部,如果调用方没有特别指定工作队列,可以在内部自动为Win32ThreadStartup填充正确的工作队列字段。
  • 这样调用方在一般情况下仍然可以像以前一样简单使用,不需要手动设置工作队列,只在需要自定义OpenGL资源的情况下再做额外配置。

总结到目前为止,完成了以下内容:

  1. 确认每个线程需要独立的Win32ThreadStartup参数,包含工作队列、OpenGL RC、OpenGL DC。
  2. 修改线程启动函数,接受并解析Win32ThreadStartup
  3. 在创建线程时,维护一个Win32ThreadStartup数组,并传入正确的数据。
  4. 添加内部便利逻辑,让不关心OpenGL细节的调用方可以自动填充工作队列。
  5. 发现并记录了项目编译设置的小问题,留待后续处理。

win32_game.cpp:初始化一些高优先级和低优先级线程,并使前两个 LowPriStartups 由 Win32GetThreadStarupForGL 填充


在之前注释掉MakeQueue调用时,实际上还没有对应的线程启动参数(startups)。
为了修正这个问题,现在需要手动创建这些Win32ThreadStartup实例。
具体做法是:

  • 定义一个high_pri_startups数组,用来存储高优先级线程的启动信息,一共有6个。
  • 使用这个数组的元素数量来决定需要创建多少高优先级线程。
  • 传递这个startups数组的指针给线程启动逻辑。

对于低优先级队列(Low Priority Queues):

  • 这些队列的线程需要额外附带OpenGL上下文信息。
  • 每个低优先级线程需要拥有一个独立的OpenGL上下文(RC)。
  • 所以还需要一个单独的low_pri_startups数组,数量对应低优先级线程数量(这里是2个)。

在处理低优先级线程时:

  • 要为每一个低优先级线程初始化一套专属的启动参数,里面包含独立的OpenGL DC和RC。
  • 这里的DC(设备上下文)和RC(渲染上下文)可以在局部变量中生成,不再需要是全局变量,这样代码结构更清晰。

具体步骤是:

  1. 调用一个新的封装函数,比如CreateWin32ThreadStartupGL,传入需要的DC和RC。
  2. 这个函数内部完成必要的OpenGL初始化并返回一个Win32ThreadStartup结构体实例。
  3. 调用两次该函数,分别为两个低优先级线程生成启动参数。

接下来,在线程创建的时候,只需要传入这些预先准备好的startups即可,而不是临时在调用时构造。

总结来说,完成了如下调整:

  • 引入了high_pri_startupslow_pri_startups数组,管理不同优先级线程的启动信息。
  • 每个低优先级线程都绑定了独立的OpenGL上下文,避免线程冲突。
  • OpenGL资源(DC、RC)改为局部管理,降低全局污染。
  • 计划编写一个新的封装函数,专门处理低优先级线程需要的OpenGL上下文初始化逻辑。
  • 整体思路是将原本分散的初始化过程集中到一个合理的、模块化的地方管理,让代码更规范。

最后,定位到需要参考之前的Win32CreateContextForWorkerThread函数,将它的逻辑提取、包装成新的初始化函数,方便直接生成线程启动参数。


移除全局变量
在这里插入图片描述

Win32CreateOpenGLContextForWorkerThread(); 可以删掉
在这里插入图片描述

win32_game.cpp:用返回 win32_thread_startup 的函数替换 Win32GetThreadStarupForGL


我们的目标是用一个新的函数替换之前的函数。新的函数要实现的功能和原来完全一样,但区别在于,它现在会把结果打包成一个线程启动结构,方便后续直接用于线程启动。

现在开始整理逻辑:

  • 我们已经实现了一个新的上下文启动逻辑,它会帮我们准备好线程启动时所需的OpenGL环境。
  • 原先手动设置的wglCreateContext返回值(旧的DC)已经不再需要,因为现在通过新的机制可以获取一个OpenGLDC,直接使用它。
  • 共享上下文(Shared Context)也由新的函数传入并绑定,所以我们也不再需要显式地维护那个共享RC。
  • 为了命名更清晰,我们将共享RC命名为shared_context,并把它传入创建函数用于生成线程专属的RC。
  • 所以旧的初始化过程中一大堆关于DC和RC管理的中间变量和流程现在都可以移除掉了。

最终逻辑简化为:

  1. 调用新的线程启动封装函数。
  2. 该函数返回一个结构体,内部已经包含初始化好的OpenGL设备上下文(DC)与渲染上下文(RC)。
  3. 我们只需将这个结构体作为线程的启动参数传入即可。

结果结构中:

  • result.opengl_dc 指向新生成的OpenGL DC。
  • result.opengl_context 是通过共享上下文派生出来的新RC。
  • 整个创建过程变得更简洁清晰,并且线程上下文初始化成为一个自动化操作。

此外,之前存在的“是否需要OpenGL”这种布尔标记也变得多余,因为现在如果我们需要线程使用OpenGL,就直接传入相关参数结构体;不需要的话,就传空。线程可以通过是否存在OpenGL上下文字段来自动判断,无需再人为打标签。

综上所述:

  • 完成了初始化函数的封装与替换。
  • 简化了上下文的管理逻辑。
  • 使线程启动参数变得结构化、可维护。
  • 移除了冗余的OpenGL相关判断逻辑,使系统更加模块化。

ThreadStartup2 的赋值封装到函数里面去
在这里插入图片描述

网络:查看 WGL_ARB_pixel_format 文档


我们在OpenGL部分还有一个细节尚未处理,那就是在设置帧缓冲区(Frame Buffer)像素格式时,是否应该显式请求sRGB支持。

之前我们在创建帧缓冲区时,通过传递一个布尔值 true 的方式,请求了 FRAMEBUFFER_SRGB_CAPABLE_ARB 属性,用于启用sRGB颜色空间支持。按照我们目前的理解,这样传参的行为,是资源申请阶段向驱动声明我们想要启用sRGB的方式。

但我们一直没有验证过:如果显式声明了 FRAMEBUFFER_SRGB_CAPABLE_ARB,但当前驱动并不支持该功能,会发生什么后果?是函数直接失败、引发错误,还是静默忽略该参数?这一点我们不确定。

于是我们回到代码中查看使用的API——wglChoosePixelFormatARB。这个函数是我们用来选择像素格式的核心,它接受两个参数列表:

  1. piAttribIList:要申请的属性ID。
  2. piAttribFList:对应属性的期望值。

我们此前在这两个列表中都填写了数值,但发现其实在某些属性上(比如浮点属性),可以填写为0表示未指定,避免意外报错。这一点之前没有注意,现在发现其实应该这么做,算是一个小的修正点。

继续查看官方文档,可以确认函数在如下情况下会失败:

  • 传入了无效的属性ID;
  • 属性列表中的值非法;
  • 或者HDC(设备上下文)本身非法。

文档中明确指出:“如果属性列表中包含非法的属性或设备上下文无效,会直接返回错误。” 这说明我们不能盲目传入 FRAMEBUFFER_SRGB_CAPABLE_ARB,除非我们事先知道驱动确实支持该功能。

因此我们得出的结论是:不能默认传入该属性。必须在使用前通过扩展检测机制确认当前驱动支持 WGL_ARB_framebuffer_sRGB,否则可能会直接导致函数调用失败甚至崩溃。

虽然这很麻烦,但确实是必须处理的事情。我们将调整逻辑:

  • 在设置像素格式前,先查询是否支持 WGL_ARB_framebuffer_sRGB
  • 若支持,则再传入 FRAMEBUFFER_SRGB_CAPABLE_ARB
  • 若不支持,则忽略该参数。

总的来说:

  • 之前认为可能会被忽略的未知属性,实际上会导致函数失败;
  • 这意味着我们必须显式检测扩展是否存在;
  • 尽管流程繁琐,但为了确保稳定性与兼容性,这是唯一可靠的做法;
  • 后续将加入对应的检测逻辑,避免在不支持的环境中触发错误。

https://registry.khronos.org/OpenGL/extensions/
https://registry.khronos.org/OpenGL/extensions/ARB/
在这里插入图片描述

game_opengl.cpp:考虑如何获取 wgl 扩展

在OpenGL中,有一种方法可以用来查询扩展功能,通常是通过 glGetString(GL_EXTENSIONS) 来完成的。但是,这个方法仅适用于查询OpenGL扩展,而不能查询WGL(Windows OpenGL扩展)扩展。因此,我们无法直接知道WGL扩展的情况。

要查询WGL扩展,需要使用 wglGetExtensionsString,这是一个不同于 glGetString(GL_EXTENSIONS) 的函数。因此,我们必须调用 wglGetExtensionsString 来获取WGL扩展的详细信息。

另外,虽然我们可以直接检查OpenGL扩展列表来看看是否支持 GLX_FRAMEBUFFER_SRGB,如果存在这个扩展,那么很可能系统就支持该功能。但实际上,由于OpenGL允许将纹理用作帧缓冲,因此即使某个实现声明支持 GL_FRAMEBUFFER_SRGB,也不能保证它在实际的帧缓冲上支持该扩展,可能只在渲染到纹理(render to texture)时才支持这个扩展。

为了做完全的验证,最好还是执行一个额外的步骤,去查询WGL扩展字符串,确认它是否支持这个功能。虽然这个过程有些麻烦,但为了确保功能的正确性,还是需要进行这个检查。

总的来说,为了验证是否支持特定的WGL扩展,必须用 wglGetExtensionsString 来进行查询,避免仅依赖于OpenGL扩展的检查。

网络:查看 WGL_EXT_extensions_string 文档,确认要检查是否有 wgl 扩展,必须先有 wgl 扩展

为了检查WGL扩展是否存在,首先需要注意的是,操作过程比较麻烦,因为在检查一个特定的WGL扩展之前,我们必须首先调用 wglGetExtensionsString 来获取扩展的字符串。这个方法返回一个包含所有WGL扩展的字符串。

问题在于,在调用 wglGetExtensionsString 之前,我们无法直接检查WGL扩展是否存在,因为字符串本身并未加载。因此,唯一的办法就是调用 wglGetExtensionsString,并获取一个指向扩展字符串的指针。之后,再根据返回的内容来判断是否存在我们想要的扩展。

简而言之,在获取扩展字符串之前,我们无法知道特定扩展是否存在,因此只能通过调用该函数并检查返回的字符串来确定是否有该扩展。这种做法虽有些麻烦,但也是目前可行的解决方案。

win32_game.cpp:使用 wglGetExtensionsStringEXT 查看我们有哪些扩展字符串

首先,在检查是否支持某个WGL扩展时,遇到了一个有趣的问题。为了检查某个扩展是否存在,必须通过 wglGetExtensionsString 函数获取扩展的字符串。如果返回的字符串中包含所需的扩展信息,那么就可以确定该扩展是否被支持。问题在于,不能直接查询这个扩展是否存在,因为没有办法预先知道扩展字符串是否已经加载。

接下来,我们通过调用 wglGetExtensionsString 获取扩展字符串后,使用一个循环检查字符串中是否包含特定的扩展。如果找到了对应的扩展,就会设置相应的标志(例如SRGB支持的标志)。为了实现这一点,需要在代码中搜索是否包含目标扩展字符串。如果找到了,就可以设置标志为 true,表示该扩展被支持。

同时,由于这段代码涉及到多个全局变量,在操作时为了提高代码的可读性和易维护性,可以考虑将这些全局变量封装到一个结构体中,这样能让代码更清晰。

如果找到所需的扩展,接着会根据支持的扩展修改后续的处理逻辑。例如,若发现OpenGL支持特定的 SRGB 帧缓冲扩展,则在传递给后续函数时需要确保该扩展已经启用。为此,如果OpenGL不支持该扩展,可以选择将相关的设置清除掉。

最后,代码中还需要查询具体的OpenGL支持的扩展字符串,这样可以进一步确认哪些特性在当前环境中可用。为了完整性,仍然需要查明OpenGL中具体的扩展标识符(例如framebuffer sRGB的扩展标志),以便正确判断和设置。

https://registry.khronos.org/OpenGL/extensions/EXT/WGL_EXT_extensions_string.txt
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

网络:查看 ARB_framebuffer_sRGB

首先,需要检查OpenGL扩展是否支持framebuffer sRGB。如果在扩展字符串中找到了这个标识符,那么可以确认当前环境支持framebuffer sRGB,从而继续后续操作。

接着,在获取这个扩展字符串之后,继续检查是否可以安全地使用该扩展。如果找到了framebuffer sRGB的支持标志,就可以确保后续的图形操作能够正确处理带有sRGB颜色空间的帧缓冲。

在此过程中,还需要确定获取扩展字符串的函数原型,并将其正确地引入到代码中。为此,可以使用 wglGetExtensionsString 函数获取扩展信息。如果之前未定义该函数的原型,需要确保在代码中声明并使用它。

另外,有一点小的修改是,将变量名更改为更具描述性的名称,例如将与扩展相关的变量命名为 extensionString,这样可以提高代码的可读性和易于维护性。

通过这些步骤,代码可以顺利执行,确保正确处理和检测OpenGL扩展,尤其是framebuffer sRGB的支持,并根据检测结果执行相应的操作。

调试器:检查我们的扩展

首先,通过查看扩展字符串代码,成功获得了图形卡的扩展信息。根据输出,卡支持多个OpenGL扩展,包括像wglChoosePixelFormatARBGL_ARB_framebuffer_sRGB等功能。特别地,framebuffer_sRGB扩展存在,并且被正确检测到。这是一个好消息,表示当前的硬件和驱动支持sRGB帧缓冲。

但是,在解析过程中发现了一些问题,最初代码没有检测到所需的扩展字符串,导致未能正确设置相关标志。通过修正后,发现并非所有扩展字符串都匹配,特别是ARB版本和EXT版本之间的差异,后者更常见且支持的范围更广。因此,应该检查EXT版本,而不是ARB版本。

修改代码后,成功地检测到支持的扩展,并且相关的标志被正确地设置为1,表示该功能被启用。这表明代码已经顺利地识别并启用了sRGB帧缓冲,且没有破坏其他兼容性设置,所有操作都按预期进行。

至于接下来的步骤,考虑到现在已经没有更多的OpenGL相关任务需要处理,除了完成一些后续的小调整外,当前阶段似乎没有其他更紧急的事项。剩余的时间可以用于验证或测试已有的功能,确保各项操作的稳定性。
在这里插入图片描述

在这里插入图片描述

WGL_EXT_depth_float WGL_ARB_buffer_region WGL_ARB_extensions_string WGL_ARB_make_current_read WGL_ARB_pixel_format WGL_ARB_pbuffer WGL_EXT_extensions_string WGL_EXT_swap_control WGL_ARB_multisample WGL_ARB_pixel_format_float WGL_ARB_framebuffer_sRGB WGL_ARB_create_context WGL_ARB_create_context_profile WGL_EXT_pixel_format_packed_float WGL_EXT_create_context_es_profile WGL_EXT_create_context_es2_profile WGL_NV_DX_interop WGL_NV_DX_interop2 WGL_ARB_robustness_application_isolation WGL_ARB_robustness_share_group_isolation WGL_ARB_create_context_robustness WGL_ARB_context_flush_control
在这里插入图片描述

todo.txt:查看 TODO 列表

当前查看了任务列表,注意到有一项是“在重新加载DLL之前刷新所有线程队列”。这是一个可以尝试去完成的任务,感觉在剩余的时间内实现的可能性比较大。

回顾之前的内容,发现关于调试代码、音频评估渲染以及硬件渲染的工作已经大部分完成。其中包括了对sRGB帧缓冲和纹理相关的处理,还有纹理数据下载的优化。不过,关于“渲染到纹理(render to texture)”这一块,目前还没有真正去实现。虽然可以现在尝试去做,但由于现阶段没有现成的系统功能使用到“渲染到纹理”,而且设计一个用于测试的渲染到纹理的用例可能也比较复杂,考虑到只剩下大约二十五分钟,贸然开始渲染到纹理的工作可能超出时间范围,因此不太适合现在进行。

简单回顾“渲染到纹理”的概念,就是不再直接渲染到屏幕帧缓冲,而是渲染到一个纹理对象,以便后续渲染步骤中作为源纹理进行采样使用。虽然概念简单,但在OpenGL中实际操作涉及较多细节配置,因此实施起来容易变得繁琐。

综合考虑,当前决定优先完成“在重新加载DLL之前刷新所有线程队列”这一任务。这个任务涉及的范围局限在现有的平台抽象层(Win32层),属于当前的工作区域内,不需要大幅度切换环境,预计也不会像渲染到纹理那样复杂,因此在剩余时间内完成的可能性更高。

总结来说,目前的计划是:

  • 不进行渲染到纹理的开发,留待后续更充分的时间来处理;
  • 优先处理“刷新所有线程队列以便在重新加载DLL前保持一致性”的工作;
  • 保持在Win32平台层工作,继续完成剩余的小型、收尾性质的任务,以便整体流程更加完整、稳健。

运行游戏并测试热重载功能

目前我们正在查看“在重新加载 DLL 前刷新所有线程队列”的具体实现情况,并尝试理解目前的实时代码加载(Live Code Reloading)机制是否已经完善,是否会带来潜在问题。

首先做了一个简单的测试:直接在编辑器中重新编译当前的 DLL,并观察游戏是否还能正常运行。测试表明游戏依然运行正常,说明 DLL 的热重载机制基本是有效的。

接下来为了进一步验证,修改了 game.cpp 中的一些与角色渲染相关的代码片段,并重新编译。这次我们可以通过屏幕上的可视化变化来验证热重载是否起效。结果也证明,在修改代码并重新编译后,游戏确实反映了代码变化,说明热重载机制在大部分情况下是正常工作的。

不过目前存在两个已知的问题,我们需要修复:


第一个问题:调试系统中字符串指针失效

调试系统中存在的问题是,当 DLL 被重新加载后,调试系统可能仍然保留了指向旧 DLL 中字符串的指针。这是因为字符串本身是存储在 DLL 的地址空间中的,而 DLL 被重新加载后,原来的地址可能就不再有效了,因此不能再使用旧的指针访问字符串数据。

这可能导致调试菜单中的字符串渲染异常,甚至出现错误或崩溃。

正确的做法应该是在 DLL 重载前,将调试系统中引用的字符串复制出来,或者在重载后重新初始化需要的字符串内容,确保指针始终指向有效的内存。


第二个问题:任务队列(Work Queue)中使用了函数指针

另一个问题出现在任务队列(Work Queue)的实现中。

当前的多线程任务系统允许主线程将任务加入任务队列,并由工作线程异步执行。这些任务本质上是以函数指针的方式被调度执行的。然而这些函数指针也往往指向的是 DLL 中的函数实现。一旦 DLL 被重新加载,原来的函数地址将不再有效,从而使得工作线程中残留的任务指针失效,造成程序错误甚至崩溃。

目前代码中存在一个处理任务队列的函数 Win32DoNextWorkQueueEntry,这部分代码就是任务调度和执行的关键。这里如果没有在 DLL 热重载前清空所有队列任务,或没有同步等待所有工作线程完成,都会造成安全隐患。


当前的结论和后续计划

目前来看,热重载机制基本可用,但存在两个关键的潜在bug:

  1. 调试系统字符串未做内存安全转移
  2. 任务队列中函数指针未做线程同步清理

接下来的目标是修复这两个问题。优先可以处理第二个问题:在重新加载 DLL 前刷新所有线程队列。我们将确保在 DLL 被卸载之前,所有工作线程完成其任务,并清空任务队列,避免使用到已失效的函数指针。

这项工作会集中在 Win32 平台抽象层进行,属于当前模块的直接范围内,适合在接下来的有限时间中完成。

win32_game.cpp:注意到 Entry.Callback 在不同版本之间位置不同,并考虑如何解决这个问题

当前我们正在深入分析 DLL 热重载过程中的潜在问题,尤其关注任务队列中的函数指针回调机制带来的崩溃风险。


目前任务队列中的每一个任务结构体都包含一个回调函数指针,该指针指向 DLL 中实现的具体函数。然而,这会在 DLL 被重新加载之后引发一个严重的问题:旧的任务队列中可能还残留着指向旧 DLL 中代码的回调函数。当新的帧开始执行时,如果这些旧任务还未被清空,系统会调用这些已经失效的回调函数,导致跳转到一段无效或错误的内存区域,从而引发程序崩溃或不确定行为。

也就是说,在旧 DLL 卸载、新 DLL 加载之间,如果线程还在执行旧任务中的回调逻辑,就会出现访问非法地址的风险。目前的系统中并没有机制来防止这种情况发生。


对此提出了两个可能的解决方案:

解决方案一:取消函数指针,统一入口分发任务

思路是取消每个任务结构中使用函数指针的方式,转而将所有任务的处理集中到一个统一的函数入口处。在这个统一入口函数中,通过一个 switchif-else 结构,根据任务类型的枚举值跳转到相应的任务处理逻辑。

这样一来,所有任务执行的入口地址将固定,DLL 重载过程中不会涉及跳转到非法函数地址的问题。

但该方案的缺点是:尽管任务入口地址固定了,但仍有可能存在另一个问题——某个工作线程正在执行 DLL 中的某段代码逻辑(即使不是通过回调跳转的方式),这时如果强制卸载 DLL,同样会导致程序崩溃。因此,单靠统一入口无法完全避免潜在风险。


解决方案二:在重新加载 DLL 前等待所有任务执行完毕

这个方案是目前更可行也更安全的方式:

  1. 在执行 DLL 热重载之前,先调用一个同步函数,等待所有工作队列中的任务全部执行完成
  2. 对所有存在的工作队列调用 Win32CompleteAllWork() 函数。
  3. 该函数会阻塞当前主线程,直到所有队列任务都执行完毕,所有工作线程空闲。
  4. 一旦任务执行完成,就可以安全地卸载旧的 DLL 并加载新的 DLL。

这种方式能够从根本上确保 DLL 被卸载时,没有任何线程仍在执行其内部代码,从而规避访问非法代码地址的崩溃问题。

目前在代码中已经明确指出,DLL 重载操作应当在某处(例如调用 Win32LoadGameCode() 前)加入一个逻辑判断或封装,确保对所有任务队列调用 Win32CompleteAllWork()


总结

热重载过程中使用函数指针的任务调度方式存在天然风险,因为 DLL 卸载之后这些指针将变为无效。

解决这一问题的最直接办法就是:在每次执行 DLL 重载之前,强制完成所有挂起任务,并确保所有线程空闲。这是目前最安全且实现简单的方案。

接下来的计划是,在热重载流程中集成这一步骤,并验证其在实际运行中是否能完全防止崩溃风险。

在这里插入图片描述

新的 GL 代码有个 bug,if(!),你加的这个检查并没有检查 sRGB 变量,而是检查了其他的变量

我们在调试过程中遇到了一些问题和反思,特别是关于 OpenGL 的 API 设计和实际使用中所带来的复杂性与潜在的 bug 问题。以下是我们的详细总结:


我们最初处理的是一个补全系统的 bug,最终发现确实是补全逻辑中的错误,这部分问题得到了确认。接着我们开始回顾之前在处理 OpenGL 的多线程纹理下载和帧缓冲相关代码中所遇到的困难。

在这个过程中,我们想强调一个关键问题:OpenGL 设计中存在的一些不合理之处,直接导致了我们代码中出现了大量没有实际意义、却极易出错的处理逻辑。比如:

  • 为了处理纹理数据在多线程环境中的传输和绘制,我们写了很多用于同步、缓冲管理、状态判断的代码。
  • 这些代码本质上并不增加程序的功能性,只是为了满足底层 API 的调用需求。
  • 然而,这种 API 使用方式的复杂性为开发者带来了大量容易犯错的机会。

举个例子,我们在使用某些 OpenGL 接口时,因为一个非常微妙的参数传递错误,导致了程序行为不符合预期。而这个错误本不应该发生——如果 API 设计得当,就应当允许我们直接调用接口并根据驱动是否支持自动决定是否执行。即便不支持,也应该有统一的查询机制而不是靠开发者手动试错。

这说明,API 的设计是否合理,会直接影响到开发者是否容易踩坑。一个好的 API 应该最小化这种“出错的可能性”。


关于 Vulkan 的一点预告:

我们还提到,对于 Vulkan,我们持有类似但更严厉的批评。虽然 Vulkan 本身是更底层、更接近硬件的接口,但它在 API 设计时并未特别关注“减少出错概率”这一点。相反:

  • 一个原本在 OpenGL 中可能只需要一次调用的操作,在 Vulkan 中往往需要几十行代码才能完成;
  • 这些代码大多数不是逻辑相关内容,而是各种配置、状态设置、描述结构体的填写等;
  • 每一个细节都可能出错,但 API 本身对这些出错点几乎没有任何防护。

换句话说,Vulkan 并没有在设计上提供任何“对错误免疫”的能力,反而为 bug 的产生提供了肥沃土壤。

我们认为,这种 API 设计理念是不负责任的。无论 API 是高层还是低层、通用还是专用,它都应当把“降低开发者出错的概率”视为首要目标之一。因为:

  • 开发中 bug 是常态,不是例外;
  • API 如果不以减少 bug 为导向,那它就是在有意无意中推动 bug 的蔓延;
  • 更安全、更明确、更自适应的 API 设计,是减少整个系统不稳定性的关键。

最后我们注意到服务端可能出现了一些问题,比如游戏聊天系统可能停机了,这虽然是与 API 无关的外围情况,但也提醒我们,一个系统的稳定性与其所有组件的设计质量息息相关。


总结:

  1. OpenGL 尽管在当年设计中已经算是合理,但其接口仍存在设计冗余、出错概率高的问题。
  2. 通过一个典型的纹理渲染和下载流程,我们清楚地看到了这些设计上的缺陷带来的多处隐患。
  3. 这反映出良好 API 设计的重要性:它不仅应当关注功能是否完整,更应关注如何减少开发者出错的可能。
  4. Vulkan 更是将复杂性进一步放大,在提高自由度的同时大幅提升了 bug 的可能性。
  5. 一个优秀的 API 应该始终把“帮助开发者减少 bug”作为设计的核心目标之一。

我们接下来的工作重点仍会围绕如何改进当前架构设计,提升使用安全性与开发效率。

在这里插入图片描述

说真的,我觉得你应该写一本 API 书。我从没想过那么多事情

我们认真地探讨了关于 API 设计的一些原则,并表达了编写一本关于 API 设计书籍的想法。我们意识到,很多人其实并没有真正理解或者采用我们所强调的这些设计理念,即使我们曾在讲座中多次详细阐述过这些内容。

我们觉得,确实可以写一本关于 API 的书,因为这些内容非常实用,而且目前仍然很少有开发者真正贯彻这些原则。虽然我们不确定这样的书会不会有很多人阅读,但我们坚信它是有价值的。

在我们已有的一些资料中,已经包含了绝大多数关键的 API 设计原则:

  • 清晰性优先:API 接口必须易于理解,不应要求调用者具备隐晦的内部知识。
  • 容错性设计:即使调用者使用错误的参数,也应避免引发严重错误,而是以安全方式返回错误码或警告。
  • 状态自管理:调用 API 不应依赖复杂的状态前提,API 自己应能判断当前是否允许操作。
  • 结构化扩展:新的功能不应破坏已有接口,通过版本号或结构体扩展支持未来需求。
  • 减少意图歧义:每一个 API 函数的功能必须非常明确,避免做“一大堆事情”的函数设计。

我们认为,如果遵循这些原则,就能构建出非常好用且安全的 API 接口。我们自己在设计系统时,围绕这些原则形成了一套清晰的思路,实践中也证明了它们的有效性。

然而,现在的大多数开发者或系统设计者,依然没有采用这些方法,导致了大量设计混乱、使用易错的 API。我们深知这些问题的后果,因此希望通过整理和传播这些知识,帮助更多人写出真正好用、健壮的 API 接口。

总的来说,这是一种设计哲学,不仅仅是编程的技术细节。我们未来或许会更系统地整理这些内容,作为一本关于 API 设计的参考书。

关于键盘/手柄输入,HalfTransitionCount 会超过 1 吗?如果把输入获取放到另一个线程中,这样能解决吗?

我们探讨了关于输入处理(特别是游戏手柄、键盘和鼠标输入)的实现细节,并分析了在不同线程和处理机制下,输入事件的“计数”是否可能超过 1 的问题。

目前游戏手柄输入是在每帧仅轮询一次的状态下进行的,因此它的事件数量最多为 1,不会积累多个输入事件。然而,如果将手柄输入的处理逻辑改为在一个独立线程中进行,并在更高频率下轮询,那么就有可能出现事件计数大于 1 的情况。因此,为了让未来的扩展更加灵活,API 设计中必须考虑这种可能性。

对于键盘输入,目前的实现是通过 Windows 的消息队列机制处理的。我们调用了 win32_process_pending_messages(),其中包括 win32_process_keyboard_message(),这就意味着键盘输入是通过 Windows 的消息循环(message loop)异步传递的。这样,如果系统在短时间内收到多个 WM_KEYDOWNWM_KEYUP 事件,它们会被保存在消息队列中,并依次被处理,所以键盘的事件计数有可能超过 1。

我们还提到了,不清楚操作系统是否足够频繁地抓取键盘事件,从而导致事件堆积——但理论上,这确实是可能发生的,因此必须在设计上为这种情况做好准备。

鼠标输入也可能存在类似机制,如果是通过 Windows 消息系统接收,那么在快速点击或移动的情况下也可能产生多个输入事件积压。

总结来说:

  • 当前游戏手柄输入由于仅每帧轮询一次,事件数量几乎不可能超过 1。
  • 如果未来对手柄输入的轮询频率提高,那么事件数量可能会增加,API 需要具备处理多个事件的能力。
  • 键盘和鼠标输入由于是通过 Windows 消息系统处理的,因此事件数量理论上可以超过 1,尤其在输入高频发生或处理不及时的情况下。
  • 为了保证游戏在不同输入频率和处理机制下依然运行良好,API 的设计必须具备良好的扩展性和容错能力。
  • 应该为未来可能发生的高频输入事件处理打下基础,避免因输入事件频率提升而造成程序逻辑错误或性能下降。

我们要做的是在当前保持简洁的同时,预留接口和结构空间,使得未来在性能需求变化时,不会影响现有系统的稳定性和正确性。

关于图形/Windows:为什么这么多游戏,尤其是 Source 引擎的游戏,Alt-Tab 处理不好?

很多游戏(尤其是使用 Source 引擎的游戏)在处理 Alt+Tab(切出游戏)操作时表现不佳,其核心问题主要与显示分辨率的切换有关。

我们倾向于不更改系统的显示分辨率。因为一旦改变显示分辨率,就存在许多潜在的问题。用户当前的显示器设置能够正常工作,证明显示器在当前分辨率下是可用的。如果我们主动切换到其他分辨率,就可能进入一个显示器不兼容的模式,导致画面不同步、花屏、黑屏,甚至完全无法显示。

大多数游戏在全屏运行时会尝试切换分辨率到一个更小、更适合游戏性能的值。这种做法的初衷是降低 GPU 渲染负担,提高帧率,尤其是在较老硬件或高性能要求的游戏中。但这会引发一系列问题:

  1. Alt+Tab 后系统自动恢复原始分辨率:Windows 会在用户切出游戏时自动还原桌面分辨率,然后游戏再次获取焦点时又需要切回游戏分辨率。这种来回切换本身就很不稳定,容易导致各种视觉和系统层面的异常。

  2. 桌面图标错乱、窗口位置被打乱:分辨率切换会让系统重新布局桌面和窗口,造成使用体验上的混乱。

  3. 显示器不支持的分辨率模式:一些显示器在特定刷新率或分辨率下会表现异常,例如花屏、偏色或者信号丢失,这对玩家来说非常糟糕。

我们更倾向于让游戏在不改变显示器分辨率的前提下运行。即使游戏内部渲染的分辨率较低,我们也会将其拉伸至显示器原有分辨率进行显示。虽然这样会带来一些额外的 GPU 负担(主要是缩放处理),但现代硬件完全能够承担这部分开销。

总体来说,这种处理方式有如下优点:

  • 更稳定的表现:Alt+Tab 不再引起分辨率切换,避免黑屏或系统异常。
  • 用户体验更好:不影响桌面布局,也避免因系统还原分辨率导致的视觉混乱。
  • 兼容性更强:避免和显示器硬件的分辨率兼容性问题。

虽然不能说所有问题都是游戏本身的错,Windows 对分辨率切换的支持长期以来就不理想。如果 Windows 能更好地管理分辨率模式切换,并保证游戏在全屏与窗口切换过程中状态的稳定性,那么很多游戏就不必自己处理这些麻烦,自然也不会有那么多 Alt+Tab 的问题。

最终结论是:为确保更好的稳定性和用户体验,我们认为在运行游戏时尽量不要修改显示器的分辨率,而是使用拉伸或其他图像处理方式在现有分辨率下运行游戏内容。这比依赖系统频繁切换模式要安全和高效得多。

把主窗口消息队列移到另一个线程有什么好处吗?抱歉,如果你已经实现或回答过这个问题,我还在赶进度

关于是否将主窗口消息队列(Windows Message Queue)移动到另一个线程的问题,进行了详细思考和分析,目前来看,认为没有明显的好处。

首先,从技术层面上讲,Windows 消息机制本身有一定限制:窗口创建在哪个线程,消息就会投递到那个线程的消息队列。因此,无法简单地通过代码直接把已有窗口的消息处理转移到另一个线程上。想要达到类似效果的办法是,把游戏主逻辑启动到另一个线程中去,而让创建窗口的主线程专门用来处理消息循环(如 PeekMessage 这类操作)。但无论实现细节如何变化,本质上窗口消息仍然归属于最初创建它的线程。

针对这样处理是否有实际好处,目前的观点是看不到明显的优势。原因如下:

  • Windows 消息系统自带队列机制:很多需要频繁获取输入(比如键盘输入)的场景,如果自己轮询硬件,需要自己负责频繁拉取数据,以免丢失输入。但在使用 Windows 消息队列的情况下,Windows 系统自己负责捕获并排队这些输入,只要及时处理队列,不容易丢失任何事件。因此,不需要为避免漏掉消息而把消息处理挪到其他线程。

  • 某些消息合并特性符合预期:比如 WM_PAINT 这类绘制消息,Windows 在队列里会自动合并重复消息,避免无意义的重复绘制请求。这种机制从应用角度来说是有益的,并不需要或想要处理所有原始产生的重复消息。

  • Windows 系统复杂且变化大:虽然长期有 Windows 编程经验,但依然认为自己对 Windows 知识了解只是局部的,因此也不敢完全排除极少数情况下这样做可能有意义。但至少就目前的经验和常规应用场景来看,看不到特别需要这么做的理由。

  • 消息处理的延迟与积压:如果处理消息的线程因为繁忙而不能及时清空消息队列,确实可能出现延迟积压的问题。但这种情况下,与其说是线程分配的问题,不如说是整体程序调度的问题,合理安排主线程负载和优化逻辑流程可能更合适。

总结来说,主窗口消息处理留在创建它的主线程上,通常是合理和高效的。没有发现明显需要把消息循环移到别的线程上的场景。消息队列本身已经是为了避免丢失消息设计的,因此切换线程不会带来什么优势,反而可能增加系统复杂度,带来更多同步和资源管理上的问题。当然,也不排除在极其特殊的应用需求下,可能存在细分优化空间,但那属于非常小众的情况了。

除了你已经展示过的教育用途,Windows 的 GDI 你还用过吗?

基本上不使用 Windows 的 GDI(图形设备接口)进行游戏开发,虽然在过去某些情况下偶尔用过,但在实际游戏项目中并不会依赖它。

现在的渲染路径已经分为两种:软件渲染和硬件渲染。从现状来看,实际上基本不会再用到 GDI。无论是走硬件渲染路径,还是即使选择软件渲染路径,最终也都会通过 OpenGL 渲染,所以根本不会真正去使用 GDI 绘制。唯一还会用到 GDI 的地方,就是在初始化 OpenGL 上下文时,创建必要的基本窗口和设备上下文,但真正渲染图形时,全程都是通过 OpenGL完成的。

这种做法在现代 Windows 上开发任何面向性能的图形应用时都是标准做法。GDI 本身并不是为高性能渲染设计的,真正需要性能的时候,通常只会把 GDI用来做一些必要的初始化工作,比如获取硬件绘图上下文(如 HDC),之后就直接交由 OpenGL 或 DirectX 接管。

除此之外,不会去使用 GDI 或者 GDI+,也不会去理会微软近年来推出的各种图形 API,比如 Direct2D、DirectComposition 或者其他什么名字很奇怪的新 API。对于高性能图形开发,只关心能让应用直接控制底层显卡资源的接口,比如 OpenGL、DirectX、Vulkan 或 Mantle 之类。

总结下来就是,真正关心图形性能的话,唯一合理的做法就是快速完成窗口创建和上下文初始化,然后直接进入 OpenGL、DirectX 或 Vulkan 这类底层图形 API 的世界。其他 Windows 自带的传统高层图形 API,在性能图形开发中几乎没有存在的意义。

你怎么看 Vulkan?它会替代 D3D 吗?

对 Vulkan 没有特别喜欢的感觉,同样也不喜欢 Direct3D 12。两个 API 都不让人觉得真正“好用”或“理想”,但这其实无关紧要,因为图形 API 的普及从来就不是由它们的设计质量决定的。

回顾整个图形 API 的发展历史,很少有哪个 API 是因为“它真的写得好”才成为主流的。实际决定哪个 API 成为主流的,是市场力量——谁在推动它,哪个厂商在支持它,哪个平台在默认它。比如 OpenGL 和 Direct3D,之所以成为标准,并不是因为它们在设计上有多出色,而是因为它们是 Windows 平台上为数不多能直接控制显卡的通道,仅此而已。

所以 Vulkan 是否会取代 Direct3D 或 OpenGL,其实更多取决于平台厂商和硬件厂商是否支持它,而不是它本身是否好用。换句话说,它能否胜出不取决于技术优势,而取决于战略推动。

微软以前非常强硬地通过 Direct3D 来锁定开发者到 Windows 平台,并把这看作一个重要的战略资源,他们曾经绝不会允许 Direct3D 被淘汰。甚至宁愿设法扼杀 Vulkan 的发展,也不愿看到自己控制的 API 被替代。

但近年来,微软的战略发生了变化。他们不再像过去那样把“绑定 Windows 平台”当作绝对目标,而是更在意像 Microsoft Store 这类平台收益,更倾向于跨平台、多系统运营的战略。例如 WSL(Windows Subsystem for Linux) 和对 Linux 的各种支持都说明他们不再执着于“Windows 优先”。

这也意味着,微软未来可能不会像从前那样拼死保护 Direct3D。如果 Vulkan 被更多厂商采纳并推动,微软也可能不再干预。这使得 Vulkan 是否能成为主流图形 API 的问题变得更复杂、更难预测。

至于 API 选择和性能,像 OpenGL 中的 selection buffer、ray picking、color picking 等鼠标选择方式各有优缺点。selection buffer 方法过于依赖旧的固定管线,已经不太适合现代 GPU;color picking 是一个可控性强、适用于现代渲染管线的技巧,但需要手动维护颜色编码;ray picking 则在精确度上最佳,但实现复杂度较高,适合物理空间精度要求高的场景。

总的来说,现在图形开发中真正有效的方式,往往不再依赖旧有的高层抽象 API,而是直接选择 Vulkan、DirectX 12、Metal 等更底层、更灵活的现代图形接口。最终使用什么,不是由开发者是否喜欢决定的,而是由平台和生态圈的趋势推动的。

选择缓冲 vs 光线拾取 vs 颜色拾取,哪种适合鼠标拾取?

通常,我个人偏好使用射线拾取(ray picking),主要是因为我觉得做一次额外的渲染通道(render pass)会比较昂贵。虽然渲染通道可以提供较为精准的结果,但由于每次渲染都会涉及额外的计算和资源消耗,尤其是当场景较为复杂时,成本会显著增加。而射线拾取通过直接计算射线与物体的交点,避免了多次渲染,能够在性能和精度之间找到一个比较好的平衡点。因此,相比于额外的渲染通道,我更倾向于选择射线拾取这种方式来处理物体选择问题。

检测键盘长按时,是否应该在输入结构中放一个时间戳?

对于检测按键按下的时长,可以通过在基础设施中设置时间戳来实现。具体来说,当按键第一次按下时,启动一个计时器,并在按键保持按下状态时持续更新该计时器。这样,就可以准确知道按键按下的时长。这个方法非常合理,能够有效地跟踪每次按键的持续时间,方便后续处理。

接下来你打算开发什么“功能”?

接下来要做的功能是完成调试代码,预计下周开始着手。这包括调试图表和相关的功能,目的是完善调试流程并提高可视化的效果,以便更好地分析和解决问题。

你认为 C++11、C++14、C++17 这些现代 C++ 命名法是为了掩盖语言的混乱吗?

关于现代合成命名法,如“C++11”、“C++14”、“C++17”等,观点是,它们似乎凸显了人们对于原始笑话的理解不够。比如,“C++”这个命名其实就是个笑话,源于原本的增量运算符“++”,用在“C”上并不会直接改变C的值,而是会改变其它地方的内存值,而返回的依然是C。这就像是在说“C++”是在做“某个地方”的改进,但最终返回的却还是原来的C。这种命名反映了这种幽默的复杂性。

然而,现代命名法并未完全理解这个笑话,甚至连原来的笑话本身也没完全理解。例如,“C++11”这样的命名其实并没有保留原有的笑点,因为“C++”应该意味着“C”加上一些东西,但这种命名方式并未能与原来的含义保持一致。最起码,应该是“C++11”或者类似的表达式才对,但实际的命名则没有坚持这样的逻辑。

总结来说,这种命名方式未能遵循其最初的幽默内涵,反而暴露了对原始含义的不理解,也许这正是对整个命名的讽刺所在。

是否值得为了赚钱而去学 JS 或 Python?

关于学习JavaScript和Python赚取金钱的问题,可以从不同的角度来看待。

首先,假设你学习这些编程语言是为了从事一些对社会有益的工作,而不是为了做一些可能对世界有害的事情,比如广告公司等。这种情况下,学习JS和Python以赚取收入是完全合理的。毕竟,每个人都需要生活,赚钱养活自己是基本的需求。

然而,问题会在于如果你选择的工作是帮助某些公司做一些有害的事情,比如通过垃圾广告影响用户生活或是一些侵害隐私的项目。那么,在这种情况下,就需要慎重考虑是否愿意为这些公司工作。每个人都有选择的权利,尤其是在决定如何影响社会时,我们应该意识到自己对世界的影响以及我们愿意做出怎样的决定。

作为程序员,很多人可能没有意识到自己在做道德决策,但实际上我们每一个选择都可能对社会产生影响。在决定是否接受一份工作时,应该先考虑这项工作是否会让社会变得更好,还是在某种程度上剥削了别人,把资源从更重要的事情上转移走。

因此,尽管为了生计去做一些工作是完全可以理解的,但如果这份工作涉及到从事一些不道德的事情,例如为一些剥削性的公司工作,那么就应该避免去做这种选择。总的来说,程序员在选择工作时应当考虑道德层面的责任,确保所从事的工作能为社会带来正面的影响,而不是只为盈利而做出有害的选择。

你对 SDL2 有什么看法?

关于SDL(Simple DirectMedia Layer),没有太多的使用经验,因此也没有深入的看法。SDL是一种跨平台的多媒体库,通常用于游戏开发和其他需要图形、声音、输入处理等功能的应用程序。虽然没有使用过SDL,但通常这种库可以帮助开发者简化底层的硬件接口,提供更方便的工具来处理图形显示、音频播放和输入管理等。

不过,由于没有实际使用过SDL,所以不能提供更详细的体验或具体的优缺点分析。

你打算使用什么技术做路径查找?

关于路径的技术,目前还没有确定使用哪一种。可能会考虑使用像A*算法这样的常见路径寻找方法,但目前还不确定,具体的选择可能会根据实际情况和需求来决定,可能会在后续开发过程中进一步探索并做出决策。

最近一直在做 JS,你觉得我下一步该做什么?对 JS 感到有点厌倦

如果对JavaScript感到厌倦,学习C语言是一个很好的选择。C语言可以让你更接近底层编程,它与JavaScript不同,JavaScript更多的是抽象的,虚拟的,而C语言则是更直接的,能够操作内存并与硬件进行更紧密的交互。通过C语言编程,你可以获得一种更具“实物感”的编程体验,这是非常有价值的。

Vulkan 是非常低级的;这是由多个游戏引擎和硬件开发者要求的。可能有巨大的市场需要一个友好的 API 来封装 Vulkan,帮助开发者避开这些复杂的细节。你不这么认为吗?

关于Vulkan是否低级API的问题,首先,低级与否并不取决于API的抽象层次,复杂性与是否是“低级”无关。有些高层API也可以非常复杂。比如,声音系统或者模块化的API,可能在表面上看起来很简单,但在实现时可能非常复杂。复杂性和抽象级别是两个不同的概念。

对于Vulkan是否低级的看法,认为Vulkan是低级的实际上是误解。Vulkan并不是一个低级API,尽管它比一些其他API(如OpenGL或DirectX)要靠近硬件。Vulkan其实是一个高层API,它与硬件的交互通过图形驱动程序进行,而这些驱动程序会隐藏很多实现细节。因此,从当前的实现来看,Vulkan并没有比OpenGL低级,它只是提供了一些更直接、更接近硬件状态的接口,而这些接口在未来几年里也不会比OpenGL更低级。

Vulkan的一个问题在于它并没有明确的保证,它并不会直接向GPU发送命令,也不会直接编译成硬件代码。它的抽象程度与OpenGL或DirectX差别不大。Vulkan的真正低级API应该是一个几乎没有抽象层次的API,用户需要直接管理内存、构建命令缓冲区等,这才是真正的“低级”API。

总的来说,Vulkan并不是一种低级API,它只是让开发者有更多控制,但这并不意味着它比OpenGL或DirectX更底层。Vulkan与其说是低级API,不如说它是为了提高开发者控制权而设计的,但它的复杂性并未减少,反而是通过增加了更多的API调用,使得开发者需要面对更多的管理和配置工作。

你多关注最新的技术吗?你认为学习最新的图形 API 对于独立开发来说是浪费时间吗?

关于是否应该跟随最新的技术,学习最新的工具和技术,这取决于具体的情况。如果你的目标是通过图形上的特殊性来区分自己,那么跟进最新的技术是有意义的,因为如何在图形上脱颖而出,如果不使用一些先进的技术,可能就很难实现这个目标。

然而,如果你的游戏更侧重于某种独特的艺术风格,而这种风格更多地依赖于艺术家的创作,而不是技术本身的能力,或者游戏的核心玩法更为重要,那么最新的技术可能并不是必需的。在这种情况下,追求技术的前沿可能不会对游戏的成功产生太大影响。

游戏开发中许多决策都有其利弊,选择使用现有的引擎(如Unity或Unreal)可能是最合适的选择,特别是当你的游戏需求与这些引擎非常契合时。作为开发者,最重要的不是仅仅掌握某些工具,而是能做出正确的技术选择。这意味着,除了知道如何使用某个工具或引擎外,开发者还应当了解何时应该使用它,何时不应该使用它。

如果你仅仅依赖于Unity或者OpenGL这样的工具,而没有意识到可能有更合适的选择,甚至无法评估其他工具或技术的优劣,那你会限制自己的能力,进而影响到项目的质量。一个好的开发者不仅要知道如何做,还要能够做出最佳的技术决策。

此外,跟上新技术的步伐并不意味着要把所有的技术都学习一遍,但理解各种技术的存在以及它们的用途非常重要。如果不了解Vulkan这样的图形API,而在某些情况下它可能是更好的选择,那么错过它会是一个失误。因此,了解其他技术并能判断何时需要使用它们是每个开发者必备的技能。

总的来说,作为一名优秀的程序员,重要的是能做出明智的技术决策,无论是使用现有的引擎、选择合适的API,还是在不同的开发需求之间做出权衡。

为什么程序员不应该为 Palantir 工作?

不建议程序员为某些公司工作,尤其是像“Palantir”这样的公司,因为它们的核心业务是开发数据挖掘工具,用于进行大规模的监控和数据收集,特别是针对一些特定群体,如穆斯林等。这样的公司通常会为政府和其他机构提供技术支持,这些机构可能会用这些工具进行各种侵犯隐私的活动,比如监控和数据分析,甚至可能助长不道德的行为。

从伦理角度来看,选择为这样的公司工作是不明智的,因为它可能助长了社会的不公和不道德行为。虽然作为程序员,工作时可能只关注技术和代码的实现,但作为开发者,应该时刻意识到自己所做的工作可能对社会产生的深远影响。就像科学家在参与曼哈顿计划时需要考虑自己研究成果可能被用来制造毁灭性武器一样,程序员也应该思考他们所做的技术是否可能被用来对社会造成伤害。因此,程序员在选择工作时,应当审视自己的工作是否会助长负面的社会影响,确保自己所从事的工作能够为世界带来积极的变化,而不是加剧不公。

Jon 用的 C++ 特性比你的更多吗?

John在编程时确实使用了比我们更多的C++特性。具体来说,他使用了更多的虚函数和一些C++的高级特性,虽然他不会频繁使用私有数据或模板等特性,但偶尔也会使用一些。尽管如此,他在使用C++的复杂特性时仍然相对谨慎,并不会过度依赖这些特性。

John对C++的使用比我们更为复杂一些,但他并不会过于依赖这些功能,因为他并不完全喜欢C++的设计,尤其是虚函数等特性没有完全满足他的需求。这也是他自己开发语言(例如JAI)的原因之一。他希望能设计出一种语言,能够更好地实现C++中的虚函数等功能,并且避免C++的一些局限性。因此,John开发的语言在某种程度上是为了克服C++在某些方面的不足,满足他自己的编程需求。

你怎么看待新的 Snowdrop 游戏引擎?

对于新发布的Snowdrop游戏引擎,了解的信息并不多。虽然它被用在了《全境封锁》这类游戏中,但实际上并没有深入了解过这个引擎。尽管《全境封锁》看起来画面相当不错,但也不能仅凭一款游戏来评价一个引擎。现在的计算机性能非常强大,几乎所有的游戏引擎都能展现出相对不错的效果,特别是如果艺术团队的水平足够高,最终效果就会显得更加优秀。所以,仅凭游戏画面并不能完全判断一个引擎的好坏。
在这里插入图片描述

考虑到人们声称 Vulkan 在性能上有差异,你打算使用 Vulkan 吗?

对于是否在项目中使用Vulkan,主要取决于具体的需求。对于英伟达显卡而言,OpenGL已经能够提供非常好的性能,因此并不需要额外使用Vulkan。而对于AMD显卡,情况则有所不同,因为它们的OpenGL驱动通常表现较差,因此可能需要使用Vulkan来提升性能。

然而,作为一个小型开发者,如果只是为了在AMD显卡上提升性能,而必须投入大量精力去实现Vulkan的支持,那么这可能不是一个明智的决定。相比之下,专注于改善互动小说的表现或者其他更具创意性的方面,可能更为值得投资。

总的来说,Vulkan对于项目的支持并不会带来实际的好处,因此目前没有太大动力去支持它。

既然你从头开始写所有东西,为什么不使用 Go 这种语言呢?

关于为什么不使用Go语言,而选择从头开始编写代码,主要是因为Go语言是一个非常高层次的语言。这种语言虽然有很多方便的特性,但并不符合个人的偏好。高层次语言的特性虽然有很多,但它们的抽象和某些语言特性并不符合个人的需求,特别是对于那些更倾向于使用低层次特性的开发者来说。因此,尽管Go语言在很多应用中非常流行,但它并不适合每个开发者,特别是那些更喜欢具有更多控制和灵活性的开发者。

最近玩了什么游戏?感觉怎么样?

最近并没有玩太多游戏。主要玩了一些小游戏,但并没有觉得一直玩游戏是个好主意。因此,最近没有花很多时间在游戏上,反而更专注于其他事情。

你有过 POSIX Linux 系统调用(例如 fork())的经验吗?我需要创建一个小的进程调度器,但当我在 for 循环中使用 fork() 时,它会出问题。我需要创建 N 个进程,然后将它们保存到 FIFO 列表中并一个个执行

在讨论使用Linux的fork系统调用时,提到如果在for循环中调用fork可能会出现问题。一般来说,fork调用在for循环内不应该导致问题。fork的作用是创建一个新进程,它会克隆当前进程并使用“写时复制”(copy-on-write)机制管理内存。因此,调用fork时,父进程和子进程会共享内存,直到有一个进程修改该内存。父进程和子进程会分别返回不同的值,父进程返回子进程的PID,而子进程返回0。

如果在for循环中调用fork时遇到问题,可能是其他部分的代码出了问题。重要的是要确保在进程中正确地判断是父进程还是子进程,通常通过fork返回值来区分。子进程可以在适当的时候使用break语句退出循环,而父进程则可以继续执行循环。

总的来说,forkfor循环中应该正常工作,除非代码中有其他逻辑错误。

你想成为第一个玩 JAI 的人吗?还是等它更成熟一点再玩?

是否愿意成为首批体验新编程语言的人,主要取决于其发布时的时间安排。如果发布时恰好有空闲时间,并且不在忙碌的项目中,可能会考虑尝试一下。但如果此时正忙于其他任务(例如正在进行的项目),那就可能没有时间去深入研究新语言。因此,是否尝试并不完全取决于对新语言的兴趣,而更多取决于当时的时间安排。

如果真的要深入了解一个新编程语言,单纯地快速浏览是不够的。必须投入相当的时间进行实践,因为仅凭外部的分析无法全面理解语言设计的决策。通过实际使用,可以更好地评估这些决策是否有效,是否能够带来实际的好处。很多时候,设计的决策背后有深思熟虑的原因,即便这些决策和原本的预期不同,最终的结果可能还是值得的。所以,要真正理解一个新语言,必须花时间去实践、去体验,而不仅仅是从表面看。

fork 是一个好的 API 吗,还是坏的 API?

关于fork API是否是一个好的选择,这取决于它的使用目标。如果目标是通过fork来创建一个新进程,那么它可能是一个合适的选择。然而,如果目标是以其他方式创建更多进程,那么fork就不一定是最好的选择,因为它的工作方式是复制当前进程的内存,这会引入不必要的开销,尤其是在需要处理页面复制的情况下,这种操作会变得非常沉重。

通常情况下,fork会复制进程的内存,这可能不符合所有的需求,特别是当只需要创建新进程而不需要复制现有进程的内存时。由于这种行为的代价较高,因此在某些场景下不推荐使用fork。然而,如果其设计的目的是为了实现进程克隆,那么它的确是一个合适的选择。

总的来说,是否将fork视为一个好的API,取决于当初设计它时的具体目标和需求。如果目标是创建进程克隆且没有其他更轻量级的替代方案,那么fork可能是合适的,但如果目标是更高效地创建进程,则fork就不太理想。

http://www.xdnf.cn/news/156133.html

相关文章:

  • 如何给GitHub项目提PR(踩坑记录
  • windows下查看idea运行的进程占的JVM情况工具
  • olama部署deepseek模型
  • 从后端研发角度出发,使用k8s部署业务系统
  • gradle-缓存、依赖、初始化脚本、仓库配置目录详解
  • SpringBoot实现的后端开发
  • Ubuntu20.04 Ollama 配置相关
  • c++初始化数组
  • C语言中位段的应用
  • 【教程】Docker运行gitlab容器
  • 数据结构和算法(八)--2-3查找树
  • 什么时候使用Python 虚拟环境(venv)而不用conda
  • Qt软件开发-摄像头检测使用软件V1.1
  • python 与Redis操作整理
  • 血泪之arduino库文件找不到ArduinoJSON.h: No such file or directory错误原因
  • 学习记录:DAY18
  • AI日报 - 2025年04月26日
  • Yocto项目实战教程-第8章-树莓派启动定制镜像-8.4小节-使用Wic工具创建分区镜像
  • 毕业项目-基于java的入侵检测与防御系统
  • 字节 AI 原生 IDE Trae 发布 v1.3.0,新增 MCP 支持
  • 使用MyBatis注解方式的完整示例,涵盖CRUD、动态SQL、分页、事务管理等场景,并附详细注释和对比表格
  • Java爬虫入门:从网页抓取到数据提取(正则表达式篇)
  • 单例设计模式之懒汉式以及线程安全问题
  • 【计算机视觉】CV项目实战- 深度解析TorchVision_Maskrcnn:基于PyTorch的实例分割实战指南
  • 从“拼凑”到“构建”:大语言模型系统设计指南!
  • 【Vue】Vue3项目创建
  • 美团Java后端二面面经!
  • 【数论分块】数论分块算法模板及真题
  • # 家庭网络IPv6地址的一些知识
  • 思科路由器重分发(静态路由+OSPF动态路由+RIP动态路由)