diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb363c8b82..ad896c516b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: build -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: linux-gcc-clang: @@ -30,13 +30,15 @@ jobs: export CC=clang export CXX=clang++ fi - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1 + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_FLAGS=-msse4.1 -DCMAKE_CXX_FLAGS=-msse4.1 -DPS2X_IOP_BUILD_TESTS=ON - name: Build run: cmake --build build --config Release - name: Tests - run: ./build/ps2xTest/ps2x_tests + run: | + ./build/ps2xTest/ps2x_tests + ./build/ps2xIOP/ps2_iop_import_tests windows-msvc: strategy: @@ -63,13 +65,14 @@ jobs: - name: Build shell: cmd run: | - cmake -B build + cmake -B build -DPS2X_IOP_BUILD_TESTS=ON cmake --build build --config Release - name: Tests shell: cmd run: | .\build\ps2xTest\Release\ps2x_tests.exe + .\build\ps2xIOP\Release\ps2_iop_import_tests.exe - name: Upload artifact uses: actions/upload-artifact@v6 with: diff --git a/ps2xIOP/README.md b/ps2xIOP/README.md index 2f47d9e7d4..1ef7c45803 100644 --- a/ps2xIOP/README.md +++ b/ps2xIOP/README.md @@ -55,3 +55,6 @@ ctest --test-dir out/build/iop-tests --output-on-failure The suites cover IRX execution, RPC, imports, version resolution and generic HLE compatibility. `ps2x_tests` also covers runtime SIF RPC/DMA integration. + + +The IOMAN compatibility layer currently supports read-only `open`, `close`, `read`, and `lseek` operations through the host VFS adapter. Write and directory operations are not implemented by this layer yet. diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.cpp b/ps2xIOP/src/emulator/imports/iop_ioman.cpp index 3eb41190f5..8ab9cb309f 100644 --- a/ps2xIOP/src/emulator/imports/iop_ioman.cpp +++ b/ps2xIOP/src/emulator/imports/iop_ioman.cpp @@ -8,13 +8,19 @@ namespace ps2x::iop::detail { - IopIoman::IopIoman(IopMemory &memory) noexcept - : m_memory(memory) + IopIoman::IopIoman(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) { } void IopIoman::reset() { + for (const auto &[fd, file] : m_openFiles) + { + (void)fd; + m_host.closeHostFile(file.hostHandle); + } + m_openFiles.clear(); m_devices.clear(); } @@ -29,6 +35,156 @@ namespace ps2x::iop::detail switch (ordinal) { + case 4: // open + { + constexpr uint32_t kAccessModeMask = 0x0003u; + constexpr uint32_t kReadOnly = 0x0001u; + constexpr uint32_t kCreate = 0x0200u; + constexpr uint32_t kTruncate = 0x0400u; + constexpr uint32_t kExclusive = 0x0800u; + + const std::string guestPath = m_memory.readString(a0, 1024u); + const uint32_t mode = cpu.gpr[5]; + if (guestPath.empty() || + (mode & kAccessModeMask) != kReadOnly || + (mode & (kCreate | kTruncate | kExclusive)) != 0u) + { + setV0(0xFFFFFFFFu); + return true; + } + + const std::string hostPath = m_host.translateGuestPath(guestPath); + if (hostPath.empty()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint64_t handle = m_host.openHostFile(hostPath); + if (handle == 0u) + { + setV0(0xFFFFFFFFu); + return true; + } + + uint64_t size = 0u; + if (!m_host.hostFileSize(handle, size)) + { + m_host.closeHostFile(handle); + setV0(0xFFFFFFFFu); + return true; + } + + int32_t fd = -1; + for (int32_t candidate = 3; candidate < 256; ++candidate) + { + if (m_openFiles.find(candidate) == m_openFiles.end()) + { + fd = candidate; + break; + } + } + if (fd < 0) + { + m_host.closeHostFile(handle); + setV0(0xFFFFFFFFu); + return true; + } + + m_openFiles.emplace(fd, OpenFile{handle, 0u, size}); + setV0(static_cast(fd)); + return true; + } + case 5: // close + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + if (file == m_openFiles.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + m_host.closeHostFile(file->second.hostHandle); + m_openFiles.erase(file); + setV0(0u); + return true; + } + case 6: // read + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + const uint32_t destination = cpu.gpr[5]; + const uint32_t requested = cpu.gpr[6]; + if (file == m_openFiles.end() || requested > IopMemory::RamSize) + { + setV0(0xFFFFFFFFu); + return true; + } + if (requested == 0u) + { + setV0(0u); + return true; + } + + std::vector buffer(requested); + size_t bytesRead = 0u; + if (!m_host.readHostFile(file->second.hostHandle, + file->second.offset, + buffer.data(), + buffer.size(), + bytesRead) || + bytesRead > buffer.size() || + !m_memory.writeRam(destination, buffer.data(), bytesRead)) + { + setV0(0xFFFFFFFFu); + return true; + } + + file->second.offset += bytesRead; + setV0(static_cast(bytesRead)); + return true; + } + case 8: // lseek + { + const int32_t fd = static_cast(a0); + const auto file = m_openFiles.find(fd); + if (file == m_openFiles.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const int32_t offset = static_cast(cpu.gpr[5]); + const uint32_t whence = cpu.gpr[6]; + int64_t base = 0; + switch (whence) + { + case 0u: // SEEK_SET + base = 0; + break; + case 1u: // SEEK_CUR + base = static_cast(file->second.offset); + break; + case 2u: // SEEK_END + base = static_cast(file->second.size); + break; + default: + setV0(0xFFFFFFFFu); + return true; + } + + const int64_t newOffset = base + static_cast(offset); + if (newOffset < 0 || newOffset > 0x7FFFFFFFLL) + { + setV0(0xFFFFFFFFu); + return true; + } + + file->second.offset = static_cast(newOffset); + setV0(static_cast(newOffset)); + return true; + } case 20: // AddDrv { if (a0 == 0u || m_devices.size() >= kMaxDevices) diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.h b/ps2xIOP/src/emulator/imports/iop_ioman.h index 182f786b17..aa2468f11e 100644 --- a/ps2xIOP/src/emulator/imports/iop_ioman.h +++ b/ps2xIOP/src/emulator/imports/iop_ioman.h @@ -1,6 +1,9 @@ #pragma once +#include "ps2x/iop/iop_host.h" + #include +#include #include #include @@ -13,7 +16,7 @@ namespace ps2x::iop::detail class IopIoman { public: - explicit IopIoman(IopMemory &memory) noexcept; + IopIoman(IopHost &host, IopMemory &memory) noexcept; void reset(); [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); @@ -26,7 +29,16 @@ namespace ps2x::iop::detail std::string name; }; + struct OpenFile + { + uint64_t hostHandle = 0u; + uint64_t offset = 0u; + uint64_t size = 0u; + }; + + IopHost &m_host; IopMemory &m_memory; std::vector m_devices; + std::map m_openFiles; }; } diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp index e5f63a9fe2..c95981cf6f 100644 --- a/ps2xIOP/src/emulator/iop_emulator.cpp +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -105,7 +105,7 @@ namespace ps2x::iop::detail heaplib(memory), intrman(memory), timrman(), - ioman(memory), + ioman(host, memory), cpuCore(memory), imports(memory), loadcore(memory, imports) diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp index 3035618443..d727075413 100644 --- a/ps2xIOP/tests/iop_import_tests.cpp +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -4,11 +4,13 @@ #include "emulator/imports/iop_cdvd.h" #include "emulator/imports/iop_imports.h" #include "emulator/imports/iop_loadcore.h" +#include "emulator/imports/iop_ioman.h" #include "emulator/imports/iop_timrman.h" #include "emulator/services/iop_rpc.h" #include "ps2x/iop/iop_host.h" #include +#include #include #include #include @@ -16,6 +18,7 @@ #include #include #include +#include namespace { @@ -49,6 +52,62 @@ namespace void log(LogLevel, std::string_view) override {} }; + class MemoryFileHost final : public NullHost + { + public: + MemoryFileHost() + : bytes{'A', 'B', 'C', 'D', 'E', 'F'} + { + } + + std::string translateGuestPath(std::string_view path) const override + { + return path == "cdrom0:\\AUDIO\\BANK.BIN;1" ? "virtual:/AUDIO/BANK.BIN" : std::string{}; + } + + uint64_t openHostFile(std::string_view path) override + { + if (path != "virtual:/AUDIO/BANK.BIN") + return 0u; + ++openCount; + return 1u; + } + + bool hostFileSize(uint64_t handle, uint64_t &size) const override + { + if (handle != 1u) + return false; + size = bytes.size(); + return true; + } + + bool readHostFile(uint64_t handle, + uint64_t offset, + void *destination, + size_t size, + size_t &bytesRead) override + { + bytesRead = 0u; + if (handle != 1u || (!destination && size != 0u)) + return false; + if (offset >= bytes.size() || size == 0u) + return true; + bytesRead = std::min(size, bytes.size() - static_cast(offset)); + std::memcpy(destination, bytes.data() + offset, bytesRead); + return true; + } + + void closeHostFile(uint64_t handle) override + { + if (handle == 1u) + ++closeCount; + } + + std::vector bytes; + uint32_t openCount = 0u; + uint32_t closeCount = 0u; + }; + class CdRootHost final : public NullHost { public: @@ -98,6 +157,88 @@ namespace return false; } + bool testIomanReadOnlyFileOperations() + { + MemoryFileHost host; + IopMemory memory; + IopIoman ioman(host, memory); + RecordingExecutor executor; + + constexpr uint32_t pathAddress = 0x2000u; + constexpr uint32_t firstBuffer = 0x2100u; + constexpr uint32_t secondBuffer = 0x2120u; + const char guestPath[] = "cdrom0:\\AUDIO\\BANK.BIN;1"; + if (!expect(memory.writeRam(pathAddress, guestPath, sizeof(guestPath)), + "could not place the IOP test path in memory")) + return false; + + IopCpuState cpu{}; + cpu.gpr[4] = pathAddress; + cpu.gpr[5] = 0x0001u; // FIO_O_RDONLY + if (!expect(ioman.dispatchImport(4u, cpu, executor), "ioman:4 (open) was not handled") || + !expect(cpu.gpr[2] == 3u, "ioman:4 did not return the first available file descriptor") || + !expect(host.openCount == 1u, "ioman:4 did not open the translated VFS path")) + return false; + const uint32_t fd = cpu.gpr[2]; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = firstBuffer; + cpu.gpr[6] = 4u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "ioman:6 (read) was not handled") || + !expect(cpu.gpr[2] == 4u, "ioman:6 returned the wrong byte count")) + return false; + char firstRead[4]{}; + if (!expect(memory.readRam(firstBuffer, firstRead, sizeof(firstRead)), + "ioman:6 did not write into IOP RAM") || + !expect(std::string(firstRead, sizeof(firstRead)) == "ABCD", + "ioman:6 read the wrong bytes")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = 2u; + cpu.gpr[6] = 0u; // FIO_SEEK_SET + if (!expect(ioman.dispatchImport(8u, cpu, executor), "ioman:8 (lseek) was not handled") || + !expect(cpu.gpr[2] == 2u, "ioman:8 did not seek to the requested offset")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = secondBuffer; + cpu.gpr[6] = 3u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "second ioman:6 (read) was not handled") || + !expect(cpu.gpr[2] == 3u, "second ioman:6 returned the wrong byte count")) + return false; + char secondRead[3]{}; + if (!expect(memory.readRam(secondBuffer, secondRead, sizeof(secondRead)), + "second ioman:6 did not write into IOP RAM") || + !expect(std::string(secondRead, sizeof(secondRead)) == "CDE", + "ioman:8/ioman:6 did not preserve the file position")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + if (!expect(ioman.dispatchImport(5u, cpu, executor), "ioman:5 (close) was not handled") || + !expect(cpu.gpr[2] == 0u, "ioman:5 did not return success") || + !expect(host.closeCount == 1u, "ioman:5 did not close the host file")) + return false; + + cpu = {}; + cpu.gpr[4] = fd; + cpu.gpr[5] = firstBuffer; + cpu.gpr[6] = 1u; + if (!expect(ioman.dispatchImport(6u, cpu, executor), "ioman:6 did not handle an invalid descriptor") || + !expect(cpu.gpr[2] == 0xFFFFFFFFu, "ioman:6 did not reject an invalid descriptor")) + return false; + + cpu = {}; + cpu.gpr[4] = pathAddress; + cpu.gpr[5] = 0x0002u; // FIO_O_WRONLY is not supported by the read-only host adapter. + return expect(ioman.dispatchImport(4u, cpu, executor), "ioman:4 did not handle a write-only open request") && + expect(cpu.gpr[2] == 0xFFFFFFFFu, "ioman:4 accepted a write-only open request"); + } + bool testLoadcoreRebootLibraryMode() { IopMemory memory; @@ -330,8 +471,8 @@ namespace int main() { - if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() || - !testTimrmanPeriodicCallback()) + if (!testIomanReadOnlyFileOperations() || !testLoadcoreRebootLibraryMode() || + !testCdvdSpecialControl() || !testCdvdSearchFile() || !testTimrmanPeriodicCallback()) return 1; std::cout << "ps2xIOP import tests passed\n"; return 0; diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 589d76850a..ebd9000e12 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -376,6 +376,8 @@ function(ps2x_stage_ffmpeg_runtime_dlls target_name) endfunction() add_library(ps2_runtime STATIC + src/lib/gs/gs_backend.cpp + src/lib/gs/gs_opengl_backend.cpp src/lib/game_overrides.cpp src/lib/gs/ps2_gif_arbiter.cpp src/lib/ps2_audio.cpp @@ -506,12 +508,27 @@ target_include_directories(ps2_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/lib/Kernel ) -target_link_libraries(ps2_runtime PUBLIC ps2_host_backend ffmpeg ps2_iop) -target_link_libraries(ps2EntryRunner - PRIVATE - ps2_runtime +target_link_libraries(ps2_runtime PUBLIC + ps2_host_backend + ffmpeg + ps2_iop ) +# Android OpenGL ES 3.2 backend +if(ANDROID) + find_library(ANDROID_EGL_LIBRARY EGL REQUIRED) + find_library(ANDROID_GLES3_LIBRARY GLESv3 REQUIRED) + + target_link_libraries(ps2_runtime PUBLIC + ${ANDROID_EGL_LIBRARY} + ${ANDROID_GLES3_LIBRARY} + ) + + target_compile_definitions(ps2_runtime PUBLIC + PS2X_HAS_GLES32=1 + ) +endif() + if(PS2X_ENABLE_DEBUG_UI AND NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID) target_sources(ps2EntryRunner PRIVATE src/lib/ps2_debug_panel.cpp diff --git a/ps2xRuntime/cmake/EnableAndroidGLES3.cmake b/ps2xRuntime/cmake/EnableAndroidGLES3.cmake new file mode 100644 index 0000000000..05506f898a --- /dev/null +++ b/ps2xRuntime/cmake/EnableAndroidGLES3.cmake @@ -0,0 +1,52 @@ +# EnableAndroidGLES3.cmake +# +# Include this file immediately AFTER FetchContent_MakeAvailable(raylib) +# in ps2xRuntime/CMakeLists.txt. +# +# This switches Raylib's Android renderer to OpenGL ES 3.0 and fixes the +# Android EGL context request, which Raylib 5.5 otherwise hardcodes to ES 2. +# +# Scope: presentation renderer only. The PS2 GS rasterizer remains the +# existing CPU backend. This file does NOT implement GPU GS rasterization. + +if(NOT ANDROID) + message(STATUS "Android GLES3 patch skipped: target is not Android") + return() +endif() + +if(NOT TARGET raylib) + message(FATAL_ERROR "Android GLES3 patch: Raylib target does not exist") +endif() + +set(_raylib_android_source + "${raylib_SOURCE_DIR}/src/platforms/rcore_android.c") + +if(NOT EXISTS "${_raylib_android_source}") + message(FATAL_ERROR + "Android GLES3 patch: Raylib Android source not found: " + "${_raylib_android_source}") +endif() + +file(READ "${_raylib_android_source}" _raylib_android_text) + +set(_old "EGL_CONTEXT_CLIENT_VERSION, 2,") +set(_new "EGL_CONTEXT_CLIENT_VERSION, (rlGetVersion() == RL_OPENGL_ES_30) ? 3 : 2,") + +string(FIND "${_raylib_android_text}" "${_new}" _already_patched) +if(NOT _already_patched EQUAL -1) + message(STATUS "Raylib Android EGL context already supports GLES 3") + return() +endif() + +string(FIND "${_raylib_android_text}" "${_old}" _old_found) +if(_old_found EQUAL -1) + message(FATAL_ERROR + "Android GLES3 patch: expected Raylib 5.5 EGL context line was not found. " + "No source file was modified.") +endif() + +string(REPLACE "${_old}" "${_new}" _raylib_android_text + "${_raylib_android_text}") +file(WRITE "${_raylib_android_source}" "${_raylib_android_text}") + +message(STATUS "Raylib Android EGL context patched for OpenGL ES 3.0") diff --git a/ps2xRuntime/include/runtime/gs/gs_backend.h b/ps2xRuntime/include/runtime/gs/gs_backend.h index 3f00d01b5f..753bd6869d 100644 --- a/ps2xRuntime/include/runtime/gs/gs_backend.h +++ b/ps2xRuntime/include/runtime/gs/gs_backend.h @@ -1,34 +1,23 @@ #pragma once -#include "runtime/gs/gs_types.h" - #include -#include +#include -class GSRasterBackend +class GSBackend { public: - virtual ~GSRasterBackend() = default; - - virtual void Initialize(uint8_t *vram, uint32_t vramSize) = 0; - virtual void Reset() = 0; + virtual ~GSBackend() = default; - virtual void Submit(const GSPrimitiveBatch &batch) = 0; - virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0; + virtual bool Initialize( + uint8_t* vram, + uint32_t vramSize + ) = 0; - virtual void BeginTransfer(const GSTransferCommand &command) = 0; - virtual void UploadImage(const uint8_t *data, uint32_t sizeBytes) = 0; + virtual void Reset() = 0; virtual void Flush() = 0; - virtual void TextureFlush() = 0; - virtual void Sync(GSSyncReason reason) = 0; - virtual PresentationFrame Present(const GSPresentationRequest &request) = 0; - virtual bool ClearFramebuffer(const GSContext &context, uint32_t rgba) = 0; - virtual uint32_t ConsumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes) = 0; + virtual void Sync() = 0; - virtual uint32_t ReadVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) const = 0; - virtual void WriteVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value) = 0; - virtual void SnapshotVram(std::vector &out) const = 0; - virtual GSTransferSnapshot GetTransferSnapshot() const = 0; + virtual bool IsAvailable() const = 0; }; diff --git a/ps2xRuntime/include/runtime/gs/gs_opengl_backend.cpp b/ps2xRuntime/include/runtime/gs/gs_opengl_backend.cpp new file mode 100644 index 0000000000..416cb061e5 --- /dev/null +++ b/ps2xRuntime/include/runtime/gs/gs_opengl_backend.cpp @@ -0,0 +1,50 @@ +#include "gs_opengl_backend.h" + +#include + +GSOpenGLBackend::GSOpenGLBackend() = default; + +GSOpenGLBackend::~GSOpenGLBackend() +{ + Reset(); +} + +bool GSOpenGLBackend::Initialize( + uint8_t* vram, + uint32_t vramSize) +{ +#ifdef __ANDROID__ + const char* version = + reinterpret_cast( + glGetString(GL_VERSION) + ); + + if (!version) + return false; + + m_vram = vram; + m_vramSize = vramSize; + + GLint major = 0; + GLint minor = 0; + + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + + if (major < 3 || (major == 3 && minor < 2)) + return false; + + m_initialized = InitializeShaders() + && InitializeBuffers() + && InitializeTextures(); + + return m_initialized; +#else + return false; +#endif +} + +bool GSOpenGLBackend::IsAvailable() const +{ + return m_initialized; +} diff --git a/ps2xRuntime/include/runtime/gs/gs_opengl_backend.h b/ps2xRuntime/include/runtime/gs/gs_opengl_backend.h new file mode 100644 index 0000000000..aa543b9eba --- /dev/null +++ b/ps2xRuntime/include/runtime/gs/gs_opengl_backend.h @@ -0,0 +1,43 @@ +#pragma once + +#include "runtime/gs/gs_backend.h" +#include + +class GSCpuBackend; + +// Fase de integración: comprueba que existe un contexto OpenGL ES 3.2 +// y conserva el comportamiento GS actual delegando las operaciones al backend CPU. +// IMPORTANTE: todavía no rasteriza las primitivas GS en la GPU. +class GSOpenGLBackend final : public GSRasterBackend +{ +public: + GSOpenGLBackend(); + ~GSOpenGLBackend() override; + + bool IsOpenGLES32Available() const; + const char* GetOpenGLVersion() const; + + void Initialize(uint8_t* vram, uint32_t vramSize) override; + void Reset() override; + void Submit(const GSPrimitiveBatch& batch) override; + void LoadClut(const GSTex0Reg& tex0, const GSTexClutReg& texclut) override; + void BeginTransfer(const GSTransferCommand& command) override; + void UploadImage(const uint8_t* data, uint32_t sizeBytes) override; + void Flush() override; + void TextureFlush() override; + void Sync(GSSyncReason reason) override; + PresentationFrame Present(const GSPresentationRequest& request) override; + bool ClearFramebuffer(const GSContext& context, uint32_t rgba) override; + uint32_t ConsumeLocalToHostBytes(uint8_t* dst, uint32_t maxBytes) override; + uint32_t ReadVram(uint32_t psm, uint32_t base, uint32_t bw, + uint32_t x, uint32_t y) const override; + void WriteVram(uint32_t psm, uint32_t base, uint32_t bw, + uint32_t x, uint32_t y, uint32_t value) override; + void SnapshotVram(std::vector& out) const override; + GSTransferSnapshot GetTransferSnapshot() const override; + +private: + std::unique_ptr m_cpu; + bool m_gles32Available = false; + char m_glVersion[256]{}; +}; diff --git a/ps2xRuntime/src/lib/gs/gs_opengl_backend.cpp b/ps2xRuntime/src/lib/gs/gs_opengl_backend.cpp new file mode 100644 index 0000000000..8667380f77 --- /dev/null +++ b/ps2xRuntime/src/lib/gs/gs_opengl_backend.cpp @@ -0,0 +1 @@ +void GS::setRasterBackend(std::unique_ptr backend)