65.9K
CodeProject 正在变化。 阅读更多。
Home

4K (2160p) 分辨率支持 PS1、PS2、PSP 游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2018年8月5日

CPOL

29分钟阅读

viewsIcon

60259

downloadIcon

309

PCSX/PCSX2/PPSSPP 模拟器在 WPF/C# 下的 Windows 10 克隆版本,带有“触摸”控制

目录

背景

该项目的主要目的是重新设计原始的 PCSX2,为用户提供更简单、更友好的用户界面,避免复杂的控件结构。在我看来,原始项目具有复杂的用户界面。它提供了强大的工具来创建游戏的“补丁”,但作为一名玩家,我并不需要所有这些——我只需要几个按钮来启动我喜欢的游戏。另一个目的是为 Windows 10 添加触摸屏控制。

简要 PCSX2 评测

Omega Red PS2 模拟器是原始 PCSX2 项目的克隆。它在图形用户界面方面进行了显著的更改,但如果没有原始项目的上下文,可能很难理解所有这些更改。

因此,原始项目是 PCSX2 团队长期工作的成果。它在 x86 CPU 上模拟 PlayStation 2 游戏机的硬件部分,适用于 Windows、Linux 和 MacOS。它包括 Emotion Engine MIPS R5900 和 I/O 处理器 MIPS R3000 的软件转换器,这些转换器将这些命令集转换为 Intel x86 命令集。然而,CPU 架构之间的差异导致定义了三个主要的执行线程:“EE Core”用于执行 MIPS R5900 和 MIPS R3000 命令集;“MTVU”用于执行 VU1 命令中的 3D 向量运算集;“MTGS”用于执行图形合成器处理器的命令。这种多线程模型可以有效地利用三个或更多核心 CPU。

在代码层面,PCSX2 基于 wxWidgetspthreads4w 框架。wxWidgets 是一个用于创建易于移植的 GUI 的框架。pthreads4w 是一个用于在 Windows 操作系统中定义 Unix 线程接口的框架,以与 Unix 代码兼容。

最后一方面是应用程序的架构。PCSX2 具有一个加载插件的主要执行应用程序。因此,其架构可以表示如下:

我对问题的看法

在我看来,原始项目过于复杂。它包含了代码调试、创建“补丁”、日志打印、CPU 转换器配置、插件配置等工具。我认为一个更简洁、更轻量级的版本可以使其易于使用。另一个方面是“触摸”模式的控制——我真的很想拿起我的轻便 Surface Pro,花一些时间玩《最终幻想 X》或《最终幻想 XII》,但“Win95”风格的 GUI 和需要使用外部游戏手柄“毁了游戏”(嵌入式触摸键盘毫无用处)。

我做了什么?

那么,我做了什么?我决定用 WPF/C# GUI 重写一个 PCSX2 的克隆版本。WPF 是一个灵活的框架,可以编写代码以正确地与“触摸”屏幕进行交互。然而,WPF 框架基于托管代码,无法有效地直接用于转换器代码。这导致了下一个解决方案——编写一个新的 C DLL 库,其中包含模拟器工作所需的最小代码。这成了一个重大问题——原始 PCSX2 项目是一个混合架构的应用程序——很难将“业务”逻辑(模拟器核心)与“瘦”客户端(用户界面)分离。这个问题因为原始代码在几乎每个源文件中都使用 wxWidgets 的类和结构而变得更糟——这使得 wxWidgets 与 PCSX2 紧密绑定。移除对 wxWidgets 的依赖需要重写几乎所有的原始源文件——对于一个爱好项目来说太多了,而且会在静态链接层面失去与原始 PCSX2 项目的兼容性——任何修改原始代码都需要手动复制并修改到新模拟器中的代码。

WxWidget 存根

在我看来,只有一个合适的决定——编写一个简单的框架,它包含与 wxWidgets 相同的静态链接接口,但用 C++ STD 代码实现所需的功能。这看起来是一项巨大的工作,但事实并非如此。从 PCSX2 中移除对 wxWidgets 的依赖可以标记与项目 GUI 部分相关的代码,并将其从代码中移除。结果,“框架”只包含 12 个源文件。其中大多数是“存根”文件——它们根据 wxWidgets 的接口定义类,但不包含可执行代码。模拟器核心代码与 wxWidgets 框架是解绑的。

创建 PCSX2Lib

我之前写过,这个项目的主要部分是找到原始 PCSX2 中可以称为“核心”或“内核”的代码。经过一些研究,我找到了可以这样命名的文件——这些文件可以在没有 GUI 和文件系统特定上下文的情况下工作。

当然,这个“Core”不能单独工作(它是 DLL),但它可以被任何支持加载和链接 C 动态库的编程语言的任何框架使用。C# 和 WPF 是实现此目的的合适框架,我面临着为 PCSX2Lib.dll 定义正确 C 接口的问题。PCSX2Lib.dll 的代码非常难理解,我认为对于大多数读者来说,更感兴趣的是查看这个库的 C 接口,并理解每个导出函数的目的。

LIBRARY "PCSX2Lib"
EXPORTS

;    Init functions
    DetectCpuAndUserModeFunc
    AllocateCoreStuffsFunc
    ApplySettingsFunc

;    EE Core thread controls
    SysThreadBase_ResumeFunc
    SysThreadBase_SuspendFunc
    SysThreadBase_ResetFunc
    SysThreadBase_CancelFunc

    
;    MTGS thread controls
    MTGS_ResumeFunc
    MTGS_WaitForOpenFunc
    MTGS_IsSelfFunc
    MTGS_SuspendFunc
    MTGS_CancelFunc
    MTGS_FreezeFunc
    MTGS_WaitGSFunc
    
;    MTVU thread controls
    MTVU_CancelFunc
    vu1Thread_WaitVUFunc
    
;    Plugin management
    openPlugin_SPU2Func
    openPlugin_DEV9Func
    openPlugin_USBFunc
    openPlugin_FWFunc    
    setPluginsInitCallback
    setPluginsCloseCallback
    setPluginsShutdownCallback
    setPluginsOpenCallback
    setPluginsAreLoadedCallback
    resetCallbacksFunc
    setGS            
    setPAD            
    setSPU2            
    setCDVD    
    setMcd        
    setUSB            
    setFW            
    setDEV9        
    
;    Patches management
    setUI_EnableSysActionsCallback
    ForgetLoadedPatchesFunc
    inifile_commandFunc
    setLoadAllPatchesAndStuffCallback
    setSioSetGameSerialFunc
    getGameStartedFunc
    getGameLoadingFunc
    getElfCRCFunc
    VTLB_Alloc_PpmapFinc
    releaseWCHARStringFunc
    getSysGetBiosDiscIDFunc
    gsUpdateFrequencyCallFunc
    getSysGetDiscIDFunc
    
;    BIOS management
    setLoadBIOSCallbackCallback
    setCDVDNVMCallback
    setCDVDGetMechaVerCallback
    
;    Saving management
    getFreezeInternalsFunc
    getEmotionMemoryFunc
    getIopMemoryFunc
    getHwRegsFunc
    getIopHwRegsFunc
    getScratchpadFunc
    getVU0memFunc
    getVU1memFunc
    getVU0progFunc
    getVU1progFunc
    getFreezeOutFunc
    setDoFreezeCallback
    
;    Loading management
    setFreezeInFunc    
    setFreezeInternalsFunc
    setEmotionMemoryFunc
    setIopMemoryFunc
    setHwRegsFunc
    setIopHwRegsFunc
    setScratchpadFunc
    setVU0memFunc
    setVU1memFunc
    setVU0progFunc
    setVU1progFunc
    
;    Elf file management
    PCSX2_Hle_SetElfPathFunc

Init 函数

  • DetectCpuAndUserModeFunc - 检测 CPU 多媒体支持的函数
  • AllocateCoreStuffsFunc - 分配内存并初始化内部变量和转换器的函数
  • ApplySettingsFunc - 为内部变量设置新值的函数

EE Core 线程控制

  • SysThreadBase_ResumeFunc - 恢复 EE Core 线程的函数(init 状态未启动,resume 命令启动此线程)
  • SysThreadBase_SuspendFunc - 挂起 EE Core 线程的函数
  • SysThreadBase_ResetFunc - 停止 EE Core 线程并释放相关内存的函数
  • SysThreadBase_CancelFunc - 使 EE Core 线程无效的函数

MTGS 线程控制

  • MTGS_ResumeFunc - 恢复图形合成器 (GS) 线程的函数(init 状态未启动,resume 命令启动此线程)
  • MTGS_WaitForOpenFunc - 等待 GS 图形上下文初始化的函数
  • MTGS_IsSelfFunc - 检查 GS 线程以防止互斥死锁的函数
  • MTGS_SuspendFunc - 挂起 GS 线程的函数
  • MTGS_CancelFunc - 取消 GS 线程的函数
  • MTGS_FreezeFunc - 从 GS 线程保存图形上下文的函数
  • MTGS_WaitGSFunc - 等待当前图形上下文完成的函数

MTVU 线程控制

  • MTVU_CancelFunc - 取消 3D 向量单元线程的函数
  • vu1Thread_WaitVUFunc - 等待 3D 向量单元线程开始的函数

插件管理

  • openPlugin_SPU2Func - 打开 AudioOutput 上下文的函数
  • openPlugin_DEV9Func - 打开 DEV9 上下文的函数
  • openPlugin_USBFunc - 打开 USB 上下文的函数
  • openPlugin_FWFunc - 打开 FW 上下文的函数
  • setPluginsInitCallback - 设置用于从 EE Core 线程初始化的 init 回调 C 函数
  • setPluginsCloseCallback - 设置用于从 EE Core 线程关闭的回调 C 函数
  • setPluginsShutdownCallback - 设置用于从 EE Core 线程释放插件资源的释放回调 C 函数
  • setPluginsOpenCallback - 设置用于从 EE Core 线程打开的回调 C 函数
  • setPluginsAreLoadedCallback - 设置用于从 EE Core 线程检查状态的加载状态检查回调 C 函数
  • resetCallbacksFunc - 释放所有回调指针的函数
  • setGS - 设置指向 GS 外部模块的 C 指针的函数
  • setPAD - 设置指向 PAD 外部模块的 C 指针的函数
  • setSPU2 - 设置指向 AudioOutput 外部模块的 C 指针的函数
  • setCDVD - 设置指向 CDVD 外部模块的 C 指针的函数
  • setMcd - 设置指向内存卡外部模块的 C 指针的函数
  • setUSB - 设置指向 USB 外部模块的 C 指针的函数
  • setFW - 设置指向 FW 外部模块的 C 指针的函数
  • setDEV9 - 设置指向 DEV9 外部模块的 C 指针的函数

补丁管理

  • setUI_EnableSysActionsCallback - 从 EE Core 线程设置 UI 更新回调的函数
  • ForgetLoadedPatchesFunc - 释放 EE Core 中所有当前补丁的函数
  • inifile_commandFunc - 设置文本补丁到 EE Core 的函数
  • setLoadAllPatchesAndStuffCallback - 设置从 EE Core 线程加载补丁回调的函数
  • setSioSetGameSerialFunc - 设置游戏光盘序列号的函数
  • getGameStartedFunc - 获取 EE Core 游戏状态的函数
  • getGameLoadingFunc - 获取 EE Core 游戏加载状态的函数
  • getElfCRCFunc - 获取 ElfFile CRC 校验和的函数
  • VTLB_Alloc_PpmapFinc - 为补丁重新分配内存的函数
  • releaseWCHARStringFunc - 释放为 wchar_t string 分配的内存的函数
  • getSysGetBiosDiscIDFunc - 获取 BIOS 光盘 ID 的函数
  • gsUpdateFrequencyCallFunc - 在打补丁后更新 GS 上下文的函数
  • getSysGetDiscIDFunc - 获取光盘 ID 的函数

BIOS 管理

  • setLoadBIOSCallbackCallback - 设置从 EE Core 线程加载 BIOS 回调的函数。
  • setCDVDNVMCallback - 设置从 EE Core 线程保存和加载 BIOS 配置的回调函数。
  • setCDVDGetMechaVerCallback - 设置从 EE Core 线程加载 DVD 硬件序列号的回调函数。

保存管理

  • getFreezeInternalsFunc - 以字节数组形式获取内部变量的函数
  • getEmotionMemoryFunc - 以字节数组形式获取 EE Core 内存的函数
  • getIopMemoryFunc - 以字节数组形式获取 I/O 处理器内存的函数
  • getHwRegsFunc - 以字节数组形式获取 EE Core 硬件寄存器的函数
  • getIopHwRegsFunc - 以字节数组形式获取 I/O 处理器硬件寄存器的函数
  • getScratchpadFunc - 以字节数组形式获取暂存器(缓冲区)内存的函数
  • getVU0memFunc - 以字节数组形式获取向量单元 0 内存的函数
  • getVU1memFunc - 以字节数组形式获取向量单元 1 内存的函数
  • getVU0progFunc - 以字节数组形式获取向量单元 0 程序(代码)的函数
  • getVU1progFunc - 以字节数组形式获取向量单元 1 程序(代码)的函数
  • getFreezeOutFunc - 以字节数组形式获取插件内存的函数
  • setDoFreezeCallback - 设置保存插件内存回调的函数

加载管理

  • setFreezeInFunc - 以字节数组形式设置插件内存的函数
  • setFreezeInternalsFunc - 以字节数组形式设置内部变量的函数
  • setEmotionMemoryFunc - 以字节数组形式设置 EE Core 内存的函数
  • setIopMemoryFunc - 以字节数组形式设置 I/O 处理器内存的函数
  • setHwRegsFunc - 以字节数组形式设置 EE Core 硬件寄存器的函数
  • setIopHwRegsFunc - 以字节数组形式设置 I/O 处理器硬件寄存器的函数
  • setScratchpadFunc - 以字节数组形式设置暂存器(缓冲区)内存的函数
  • setVU0memFunc - 以字节数组形式设置向量单元 0 内存的函数
  • setVU1memFunc - 以字节数组形式设置向量单元 1 内存的函数
  • setVU0progFunc - 以字节数组形式设置向量单元 0 程序(代码)的函数
  • setVU1progFunc - 以字节数组形式设置向量单元 1 程序(代码)的函数

Elf 文件管理

  • PCSX2_Hle_SetElfPathFunc - 设置 Elf 文件路径的函数(不可用,但静态链接需要)

这些函数有特定的调用顺序,外部框架必须执行它们并通过特定的算法调用回调函数中的代码。

在 C# 代码中,启动模拟器从初始化开始

PCSX2LibNative.Instance.DetectCpuAndUserModeFunc();
PCSX2LibNative.Instance.AllocateCoreStuffsFunc
            (PCSX2Controller.Instance.m_Pcsx2Config.serialize());
PCSX2LibNative.Instance.PCSX2_Hle_SetElfPathFunc("");

绑定回调函数

private void Bind()
{
      foreach (var l_Module in ModuleManager.Instance.Modules)
      {
          PCSX2LibNative.Instance.setModule(l_Module);
      }

      PCSX2LibNative.Instance.setPluginsInitCallback = delegate()
      {
          ModuleControl.Instance.init();
      };

      PCSX2LibNative.Instance.setPluginsCloseCallback = delegate()
      {
          ModuleControl.Instance.close();
      };

      PCSX2LibNative.Instance.setPluginsShutdownCallback = delegate()
      {
          ModuleControl.Instance.shutdown();
      };

      PCSX2LibNative.Instance.setPluginsOpenCallback = delegate()
      {
          ModuleControl.Instance.open();
      };

      PCSX2LibNative.Instance.setPluginsAreLoadedCallback = delegate()
      {
          return ModuleControl.Instance.areLoaded();
      };
            
      PCSX2LibNative.Instance.setUI_EnableSysActionsCallback = delegate()
      {
          if (!PCSX2Controller.Instance.innerCall())
             LockScreenManager.Instance.hide();
      };

      PCSX2LibNative.Instance.setLoadAllPatchesAndStuffCallback = delegate(uint a_FirstArg)
      {
           PatchAndGameFixManager.Instance.LoadAllPatchesAndStuff();
      };

      PCSX2LibNative.Instance.setLoadBIOSCallbackCallback = 
                         delegate(IntPtr a_FirstArg, Int32 a_SecondArg)
      {
           Omega_Red.Tools.BiosControl.LoadBIOS(a_FirstArg, a_SecondArg);
      };

      PCSX2LibNative.Instance.setCDVDNVMCallback = 
                         delegate(IntPtr buffer, Int32 offset, Int32 bytes, Boolean read)
      {
          Omega_Red.Tools.BiosControl.NVMFile(buffer, offset, bytes, read);
      };

      PCSX2LibNative.Instance.setCDVDGetMechaVerCallback = delegate(IntPtr buffer)
      {
          Omega_Red.Tools.BiosControl.CDVDGetMechaVer(buffer);
      };

      PCSX2LibNative.Instance.setDoFreezeCallback = 
                         delegate(IntPtr a_FirstArg, Int32 a_mode, Int32 a_ModuleCode)
      {
          return ModuleControl.Instance.doFreeze(a_FirstArg, a_mode, a_ModuleCode);
      };
}

并恢复 EE Core 线程

PCSX2LibNative.Instance.SysThreadBase_ResumeFunc();

Omega Red 项目很复杂,审查整个代码需要一本真正的书。然而,我认为关注编程解决方案的架构很重要。

模块 - 插件

原始 PCSX2 项目定义了一组外部插件用于与模拟器交互——视频、音频、手柄、内存卡。Omega Red 项目也有类似的解决方案,称为模块——它们不存在于真实的文件中,而是作为资源包含在可执行文件中。在应用程序启动时,它们会被解包,在应用程序退出时,它们会被删除。另一个方面是定义模块接口——每个模块都有自己的一组函数供 PCSX2Lib 使用,但这对于外部框架来说是无用的——因此,在这个项目中,有一个自己的通用接口用于与模块交互。

PCSX2_EXPORT_C_(void*) getAPI()
{
    return (void*)&CDVDapi_Iso;
}

PCSX2_EXPORT_C execute(const wchar_t* a_command, wchar_t** a_result)
{
    g_CDVD.execute(a_command, a_result);
}

PCSX2_EXPORT_C releaseString(wchar_t* a_string)
{
    if (a_string != nullptr)
        delete[] a_string;
}

函数 getAPI 返回指向 PCSX2Lib API 的指针,函数 execute 执行来自外部框架的命令,并以 XML 文档的形式返回结果,函数 releaseString 释放返回结果字符串的内存。

视频渲染器

Omega Red 有一个用于渲染的 DirectX11 模块,但有一个问题——WPF 只支持 D3DImage 中的 DirectX9 纹理。这个问题通过以下方式解决:

  1. 使用“Shared”句柄创建 DirectX 纹理——Direct3D9Ex 支持使用 DXGI 共享句柄创建 DirectX9 纹理。
  2. 从 DirectX9 纹理的 DXGI 共享句柄创建共享的 DirectX11 纹理
    // Create shared texture 
    CComPtr<ID3D11Resource> l_Resource;
    hr = m_dev->OpenSharedResource(sharedhandle, IID_PPV_ARGS(&l_Resource));
    if (FAILED(hr)) return false;
    hr = l_Resource->QueryInterface(IID_PPV_ARGS(&m_SharedTexture));

触摸板

Omega Red 集成了触摸板

它是根据 XInput 规范实现的,通过定义 C# 结构体将按钮状态和模拟摇杆轴复制到本机内存中

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct XINPUT_GAMEPAD {
            public UInt16 wButtons;
          public byte bLeftTrigger;
          public byte bRightTrigger;
          public Int16 sThumbLX;
          public Int16 sThumbLY;
          public Int16 sThumbRX;
          public Int16 sThumbRY;
        };
        
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct XINPUT_STATE {
            public UInt32 dwPacketNumber;
            public XINPUT_GAMEPAD Gamepad;
        };

屏幕和视频捕捉

原始 PCSX2 具有一些用于捕获渲染的游戏图像和录制游戏视频的功能,但这些功能不易访问且未针对此目的进行优化。我决定包含更灵活的解决方案。它使用 WPF 功能通过将 DirectX 纹理从后缓冲区复制到 C# BitmapSource 并使用 JpegBitmapEncoder 进行编码,将屏幕捕获到 JPEG 格式图像文件。

public byte[] takeScreenshot()
{            
    byte[] l_result = null;

    var l_D3D9Image = imageSource as D3D9Image;

    if(l_D3D9Image != null)
    {
        BitmapSource l_bitmap = l_D3D9Image.getBackBuffer();

        JpegBitmapEncoder l_encoder = new JpegBitmapEncoder();

        l_encoder.QualityLevel = 75;

        l_encoder.Frames.Add(BitmapFrame.Create(l_bitmap));

        using (var outputStream = new MemoryStream())
        {
            l_encoder.Save(outputStream);

            l_result = outputStream.ToArray();
        }
    }

    return l_result;
}

为了捕捉游戏视频,我使用了 CaptureManager SDK——这是一个用于从许多源捕捉视频和音频的简单 SDK。对于本项目,CaptureManager SDK 具有以下优点:

  1. 简单灵活的 C# interface——它允许将视频捕获代码集成到任何 WPF/C# 项目中。
  2. 通过 XML 文档定义源——捕获 DirectX11 纹理的指针地址以文本格式写入数字。
string lPresentationDescriptor = "<?xml version='1.0' encoding='UTF-8'?>" +
"<PresentationDescriptor StreamCount='1'>" +
    "<PresentationDescriptor.Attributes Title='Attributes of Presentation'>" +
        "<Attribute Name='MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK' 
        GUID='{58F0AAD8-22BF-4F8A-BB3D-D2C4978C6E2F}' Title='The symbolic link for a 
        video capture driver.' Description='Contains the unique symbolic link 
        for a video capture driver.'>" +
            "<SingleValue Value='ImageCaptureProcessor' />" +
        "</Attribute>" +
        "<Attribute Name='MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME' 
        GUID='{60D0E559-52F8-4FA2-BBCE-ACDB34A8EC01}' 
        Title='The display name for a device.' 
        Description='The display name is a human-readable string, 
        suitable for display in a user interface.'>" + 
            "<SingleValue Value='Image Capture Processor' />" +
        "</Attribute>" +
    "</PresentationDescriptor.Attributes>" + 
    "<StreamDescriptor Index='0' MajorType='MFMediaType_Video' 
    MajorTypeGUID='{73646976-0000-0010-8000-00AA00389B71}'>" + 
        "<MediaTypes TypeCount='1'>" + 
            "<MediaType Index='0'>" +
                "<MediaTypeItem Name='MF_MT_FRAME_RATE' 
                GUID='{C459A2E8-3D2C-4E44-B132-FEE5156C7BB0}' Title='Frame rate.' 
                Description='Frame rate of a video media type, in frames per second.'>" + 
                    "<RatioValue Value='30.0'>" + 
                        "<Value.ValueParts>" + 
                            "<ValuePart Title='Numerator'  Value='30' />" +  
                            "<ValuePart Title='Denominator'  Value='1' />" +  
                        "</Value.ValueParts>" + 
                    "</RatioValue>" + 
                "</MediaTypeItem>" +
                "<MediaTypeItem Name='CM_DirectX11_Capture_Texture' 
                GUID='{179B7A05-496A-4C9F-B8C6-15F04E669595}'>" +
                    "<SingleValue Value='{Temp_Capture_Texture}' />" +
                "</MediaTypeItem>" +
            "</MediaType>" +
        "</MediaTypes>" +
    "</StreamDescriptor>" +
"</PresentationDescriptor>";

其中 {Temp_Capture_Texture} 被替换为用于用作源的 DirectX11 渲染目标纹理的指针的文本表示。

lPresentationDescriptor = lPresentationDescriptor.Replace
("{Temp_Capture_Texture}", a_PtrDirectX11Source.ToInt32().ToString()); 

音频流通过“音频回放”录制——从内部系统扬声器输出。

CaptureManager SDK 使用不同的视频和音频编码器来录制游戏视频。

依赖

Omega Red 基于 PCSX2 代码,为了在编译过程中正确链接原始代码,它被放在文件夹中。

结果二进制可执行文件放在“bin”文件夹中。

此项目的源代码存储在 GitHub 的 Omega_Red 仓库中:https://github.com/Xirexel/OmegaRed

最终结果

结果以 ONE 可执行文件的形式呈现——它以一个文件的形式包含所有必需的代码。因此,该程序专为“触摸”控制而设计,并具有以下功能:

  1. 全屏——程序只有一个窗口大小——最大化,占据整个屏幕区域。
  2. 最低配置——大多数游戏默认配置已足够。
  3. 它只允许单人游戏配置。

接下来的子部分更详细地描述了结果程序,但小 HTML 图像中的全屏图像质量较差。

用户界面

Omega Red 是一个旨在使原始 PCSX2 更友好、更易于使用的项目。这些目标反映在用户界面的设计中,该界面具有 WPF GUI 框架可提供的功能。

PCSX2 的原始用户界面为“Win95”风格

已替换为具有“触摸-瓦片”设计的新风格,以支持屏幕触摸控制,同时尽量少使用经典的“鼠标”HID。

资源管理

花费了大量时间设计了一个简单的方案,用于轻松控制和管理主要游戏资源:BIOS、ISO 游戏光盘、内存卡、手柄、存档。

BIOS 是模拟器工作过程的重要组成部分。Omega Red 支持从二进制文件或 ZIP 存档中读取数据。

ISO 光盘映像文件是在此模拟器上启动游戏的唯一方式。这些文件已在游戏光盘集合中注册并得到识别——原始 PCSX2 模拟器在启动游戏时读取和检查 ISO 文件,而这个新模拟器则在不启动模拟器的情况下检查光盘映像类型、支持的 PS2、游戏光盘区域。

加载和保存状态已大大改变。原始 PCSX2 模拟器对每个游戏限制为 10 个文件槽,而不定义保存日期和游戏进度。

Omega Red 允许在游戏过程中为每个游戏创建多达 100 个文件槽,每个文件槽保存保存日期、当前游戏会话的时长以及捕获的游戏过程图像。

此外,在停止模拟器或关闭 Omega Red 应用程序时,当前游戏进程会保存到名为“Autosave”的特殊文件中——这允许玩家在忘记保存最后一个游戏会话时继续游戏。此外,加载已保存状态的顺序也已改变——原始 PCSX2 需要加载 BIOS,加载游戏光盘,检查它,然后才需要点击所需槽,而 Omega Red 只需单击即可加载已保存的游戏状态。结果,加载已保存游戏的时间从 20 秒减少到 3-5 秒。

该项目允许更“灵活”地管理 PS2 内存卡。

可以在游戏过程中创建内存卡,并且模拟器可以在游戏过程中切换到另一张内存卡,以便在游戏中保存和加载数据。创建的内存卡文件的名称基于游戏名称和游戏光盘的唯一序列 ID——这允许仅显示“自己的”游戏内存卡文件。

手柄

Omega Red 允许在“触摸”手柄和“游戏”手柄之间切换游戏控制。

捕捉

原始 PCSX2 中捕获图像和视频的功能存在一些限制,并且需要研究配置才能找到这些命令。在这个项目中,我重新设计了它,并在显示屏顶部添加了必要的控制按钮。

捕获图像的质量是固定的(75%)。“实时”游戏视频的质量可以在 10% 到 99% 之间更改。创建的图像和视频文件的名称由游戏友好名称和当前日期生成。分离的面板允许在暂停时查看捕获的图像和视频。

通用配置

通用配置”面板允许更改非游戏配置:“显示模式”、“控制模式”、“置顶显示”、“视频压缩质量”、“关闭宽屏格式”、“当前语言”。

显示模式”允许在全屏和区域之间切换显示区域。

控制模式”允许在“按钮”和“触摸”模式之间切换通用控制。

置顶显示”将应用程序窗口置于其他应用程序之上。“视频压缩质量”允许设置视频捕获功能的质量压缩。“关闭宽屏格式”——默认情况下,Omega Red 会应用补丁来将屏幕比例更改为 16/9,但此选项允许禁用这些补丁。“当前语言:”允许更改当前语言。

当前配色方案:”允许更改当前配色方案。

触摸板缩放”允许更改游戏触摸板元素的比例,以适应屏幕尺寸和手指大小。

162% 的触摸板缩放

78% 的触摸板缩放

细分曲面

Omega Red 使用 DirectX 11 进行模拟游戏渲染。这项技术支持现代显卡许多技术。其中之一是 细分曲面——一种在运行时提高网格质量的特殊技术。

我通过添加特殊的 渲染模式 配置将细分曲面包含到 Omega Red 中。

结果如下:

默认

细分曲面

默认

细分曲面

FXAA

此选项 启用 FXAA 允许开启或关闭抗锯齿后处理。

快速保存

Omega Red 支持保存和加载当前游戏状态。我通过添加快速保存和加载命令进行了改进。

Omega Red 有 5 个快速保存槽,按队列顺序排列——最新的保存位于队列前面,最旧的保存位于队列尾部。

物理游戏手柄扩展了快速保存功能——通过物理游戏手柄上的特殊按钮组合进行快速保存和快速加载。

快速保存:Start + Left Shoulder

快速加载:Start + Right Shoulder

快速加载具有“30 秒规则”——这意味着如果下一次快速加载时间距离上一次不足 30 秒,Omega Red 将加载上一个快速保存槽。结果,可以通过反复单击组合键 Start + Right Shoulder 来查看所有当前的快速保存,以找到合适的保存!

音量控制

Omega Red 支持通过专用的滑块音量和静音按钮控制渲染音频的音量。

此组件仅在本地音频渲染器的上下文中控制音频音量,而**不更改**扬声器输出音量!

离屏模式

Omega Red 具有在个人电脑主桌面屏幕上渲染游戏视频和音频的功能。这是该 PS2 模拟器的正常模式。然而,经过一些研究,我决定改进模拟器的架构——添加**新功能,通过 LAN 通过 RTSP 协议将渲染的游戏远程显示到 Android TV 等远程设备上**。为了防止在使用个人电脑处理不同任务时可能出现的问题,我为 Omega Red 添加了一个特殊模式:“离屏”,可通过参数命令 /OffScreen 启用。此模式将视频和音频渲染输出从当前用户上下文重定向到 RTSP 服务器输入——结果,在本地个人电脑上,Omega Red PS2 模拟器在隐藏状态下启动,带有控制图标。PS2 游戏可以从任务栏图标启动。游戏控制由物理游戏手柄管理。

快速加载图标

Omega Red 保留了最后选择的游戏光盘的引用,并在下一个应用程序会话中选择它——很有可能在最后一个保存处继续上一个游戏——Autosaving。显示自动保存的图标允许通过单击该图标来继续游戏。

PSP 模拟器

大约半年前,我看到了 Steam 上的“《战场女武神 4》”。我一直对“4”这个数字感到困惑——很久以前我就玩过“《战场女武神》”。但是,“《战场女武神 2》”和“《战场女武神 3》”呢?那些游戏是在游戏机上发布的。对我来说最容易访问的是 PSP 版本,通过 PPSSPP——但我并不喜欢那个模拟器的触摸控制。此外,我不想有两个不同的程序来玩游戏并在它们之间切换。所以,我有一个想法——将 PPSSPP 模拟器添加到 Omega Red 中,并创建 PS2 和 PSP 游戏平台!我试过了,并且是可行的。

GoogleDrive 和 YouTube

经过一段时间的研究,我决定添加 Google 账户“绑定”——它允许包含一些 Google 服务的功能:GoogleDrive 和 YouTube——将游戏保存存储在“云”中,并轻松将录制的视频上传到您的 YouTube 账户。

点击 Google 图标后,应用程序将在当前默认的 Internet 浏览器中打开您的 Google 账户,以便将 Omega Red 与您的 Google 账户“绑定”。

“绑定”后,所有保存的文件都会获得 GoogleDrive 图标,以标记“云”保存访问。

还可以从 GoogleDrive 下载保存到您的计算机。

与 YouTube 账户“绑定”后,Omega Red 视频播放器中会显示一个特殊的 YouTube 图标,用于直接将当前视频上传到您的账户。

游戏直播(YouTube、Facebook、Twitch)

是的,将实时视频和音频游戏直播添加到 PS2/PSP 模拟器中,这真是个疯狂的主意。

这项任务的有趣之处在于:内部捕获视频和音频。常规的流媒体应用程序通常从屏幕捕获视频,从扬声器捕获音频。然而,我决定改变概念——在模拟器级别添加流媒体功能,以将视频和音频与 Windows 操作系统系统混音器上下文隔离开来。

流媒体功能有两个部分:

  • 配置视频和音频编码,以及选择用于直播的互联网流媒体服务。
  • 配置混音器——将实时游戏视频和音频与附加源混合:麦克风、网络摄像头、JPG/PNG/GIF(动画)文件。

配置视频和音频编码

流媒体配置从选择捕获模式 Streaming 开始。此模式允许选择流媒体的视频和音频比特率。此外,还可以选择或输入目标流媒体服务的地址。

需要注意的是,Omega Red 支持开放和加密(SSL)连接!

Facebook 的流媒体服务发布了以下公告:

“正如去年四月宣布的那样,从 5 月 1 日起,所有直播视频上传都必须使用 RTMPS(RTMP over a TLS/SSL connection),并且开发者在此日期之前确保他们的集成符合要求非常重要。

我们致力于保护 Facebook 平台的完整性,作为这项工作的一部分,我们要求直播视频上传使用 RTMPS,因为它为视频内容提供了更高程度的安全性。”

我决定支持 RTMPS 非常有前景,并花了一些时间将 OpenSSL 添加到 RTMP 库中。它是可行的!Facebook 在加密连接上接收流媒体!

流媒体从按下图标 Record/Streaming 开始。

混音器配置在录制/流媒体开始后可用。

媒体源媒体 面板中可用——这个展开器菜单包含用于混合的视频和音频源。此外,还可以添加 JPG/PNG/GIF(动画)文件进行混合。

对于麦克风,可以控制音频混合的比例。麦克风的选择通过单击复选框按钮完成,混合通过音量滑块控制。

网络摄像头的选择通过单击复选按钮完成,网络摄像头视频的分辨率通过单击组合菜单选择。

混合的透明度可以通过不透明度滑块控制。

混合视频流的位置和大小可以在流媒体时更改。

我的测试流媒体

 

PS1 模拟器

经过一段时间的思考,我决定在我的项目中加入第一个 PlayStation 系列游戏机的模拟器。然而,我遇到了 PCSX-Reloaded 开源社区停止支持的问题——代码已经停止支持大约 15-20 年了,而且将基于 DirectDraw 的图形渲染器与 WPF 和 DirectX 11 技术集成存在问题。我决定通过将 PCSX-Reloaded 的软件渲染器捕获的视频从系统内存快速复制到视频内存(使用 DirectX 11 纹理)来绑定它。这个解决方案效率不高,但它允许获得 100% 的兼容性,并且适用于第一个版本。

此外,我还添加了两个用于与 PCSX-Reloaded 兼容的功能:

  1. 视觉震动——PlayStation 的游戏手柄具有反馈功能,可以使玩家更深入地融入游戏。Omega Red 项目支持游戏手柄控制的反馈,但触摸板的反馈是 PlayStation 6 的一个奇妙功能!然而,在研究 PCSX-Reloaded 代码时,我发现了从游戏手柄模拟器到图形渲染器的回调函数的引用!我发现通过改变渲染图像来响应游戏手柄振动的想法很棒!结果,我在我的项目中添加了这项功能:                                                                                     
  2. 共享内存卡——原始 Omega Red 项目根据游戏光盘的唯一序列号为每个游戏创建内存卡,以防止一个游戏的保存覆盖另一个游戏。这种模式对 PlayStation 2 有效,因为游戏通常只使用一张 DVD,但在 PlayStation 1 游戏中,在游戏光盘之间传输游戏状态存在问题——许多 PlayStation 1 游戏需要两张或更多光盘。因此,我决定为所有 PlayStation 1 游戏添加一个共享内存卡。

此外,我还添加了三项新功能:

  1. 为实时游戏过程选择视频编码的恒定比特率:                                                                     选择目标录制文件的大小:                                                                                                                   
  2. 按组分割 PlayStation 1、PlayStation 2 和 PlayStation Portable 的 BIOS 和游戏光盘:                
  3. 在相关部分的头部显示当前 BIOS 和游戏光盘的名称:                                              

您可以在接下来的演示中看到这三个平台上的游戏演示。

 

新的 PS1 图形渲染器

经过一段时间测试 PS1 代码后,我发现软件渲染器存在局限性——在我试图将电视分辨率的结果图像扩展到高清 720p 时,我发现图像包含多边形和纹理的巨大像素伪影。合适的解决方案是用硬件渲染器替换软件渲染器。然而,我发现目前 PS1 的硬件渲染器解决方案使用 OpenGL 驱动程序——这对我的 Omega Red 项目来说是个问题,因为它基于 WPF、DirectX9 和 DirectX11。我只找到了一个解决方案——用 DirectX11 编写新的图形渲染器!

我的工作成果很好,并且非常有前景。

您可以在接下来的演示中看到这些渲染器的比较演示。

 

刷新帧率

该项目使用 WPF C# 框架作为模拟器渲染器和显示图形上下文之间的中间层:渲染器将场景绘制到纹理中,WPF 框架将该纹理嵌入到用户界面中。将 DirectX 纹理嵌入用户界面是 WPF 图形驱动程序的“瓶颈”——WPF 是为 Windows XP SP2 设计的,并且在底层使用 DirectX9 层,嵌入 DirectX9 纹理会消耗 CPU 资源。

结果,CPU 负载增加,CPU 延迟也增加——在模拟器的音频流中出现静默停顿。通过跳过 DirectX9 纹理嵌入可以减少 CPU 负载。纹理更新的当前频率可以通过选项“Show frames per second”进行监控。

帧更新可以通过选项“Skip frame mode:”进行更改。

 

4K (2160p) 分辨率支持 PS1、PS2、PSP 游戏

大多数模拟器都有软件解决方案,可以通过本机 API:DirectX 或 OpenGL 来渲染游戏场景。结果,模拟器具有不同功能的图形引擎。其中之一是使用目标输出窗口分辨率进行渲染。此解决方案使用特殊的目标视频纹理作为 PS1、PS2、PSP 模拟器的目标渲染目的地,并且该纹理的大小决定了虚拟摄像机的视角大小。

目标渲染纹理的大小在配置大小中定义。

720p / 2160p 渲染质量的比较可以在视频中看到。

恢复

Omega Red 的开发源于一个简单的想法,即改进原始 PCSX2 模拟器并添加一些新功能。它是一个更用户友好的解决方案,并且对 PCSX2 开发社区可能有用。

关注点

在开发项目 C# 部分时,我遇到了堆栈溢出问题——在处理 DVD 映像程序的数据时,程序会抛出堆栈溢出异常。经过一些研究,我发现原始 PCSX2 项目通过 C/C++ 宏的组合生成 MIPS 转换器的代码。这允许快速生成高效的代码,但这段代码具有递归结构,会消耗进程的 STACK。对于本机 C/C++ 来说,这不是问题,但 C# 项目的 STACK 大小有限,不足以处理 PCSX2 代码。它是如何解决的?——很简单。Visual Studio 套件中有一个名为“editbin.exe”的特殊程序,允许编辑编译后的程序——“/STACK:”参数允许更改进程的 STACK。以下命令在“Post-build event command line”中执行:

"$(DevEnvDir)..\..\VC\bin\editbin.exe" /STACK:6000000 "$(TargetPath)"

它将 C# 程序的 STACK 扩展到 600 万字节。

历史和更新

2018/09/10:首次更新

  • 添加了“当前配色方案:”选项以更改应用程序元素的颜色。

2018/11/19:第二次更新

  • 将 Omega Red Visual Studio 项目从 VS2013 迁移到 VS2017,支持 VideoRenderer 项目中 GSdx 的最新更新。
  • 添加了“触摸板缩放”选项,用于更改游戏触摸板元素的尺寸,以适应屏幕尺寸和手指大小。
  • 添加了 MediaStream 项目,用于实现当前游戏“实时”流功能的 RTSP 服务器。

2019/02/11:第三次更新

  • 添加了“细分曲面”配置。
  • 添加了“音量控制”组件。
  • 添加了“快速保存”组件。
  • 添加了通过 RTSP 服务器 支持的远程显示。

2019/04/02:第四次更新

  • PCSX2 添加了“启用 FXAA”。
  • 添加了“快速加载图标”。
  • 添加了 PPSSPP 模拟器。

2019/06/27:第五次更新

  • 添加了 GoogleDrive 和 YouTube 支持。
  • 添加了游戏直播功能。

2019/11/25:第六次更新

  • 添加了 PlayStation 1 的 PCSX-Reloaded 模拟器。
  • 添加了“启用了视觉震动”。
  • 添加了共享内存卡。
  • 添加了视频编码器比特率选择。
  • 添加了录制文件大小选择。
  • 添加了 BIOS 和游戏光盘分组。
  • 添加了在相关部分头部显示当前 BIOS 和游戏光盘的名称。

2020/01/21:第七次更新

  • 将 PlayStation 1 模拟器中的软件渲染器替换为 DirectX 11 渲染器。

2020/07/13:第八次更新

  • Omega Red 视频指南;
  • 添加 刷新帧率
  • 支持游戏手柄。

2020/08/03:第九次更新

  • 添加了渲染分辨率选择:710p、1080p、1440p、2160p。

 

© . All rights reserved.