Merge branch 'main' of https://github.com/Xphalnos/shadPS4
This commit is contained in:
commit
edf6724e74
|
@ -190,6 +190,10 @@ set(PAD_LIB src/core/libraries/pad/pad.cpp
|
||||||
src/core/libraries/pad/pad.h
|
src/core/libraries/pad/pad.h
|
||||||
)
|
)
|
||||||
|
|
||||||
|
set(PNG_LIB src/core/libraries/libpng/pngdec.cpp
|
||||||
|
src/core/libraries/libpng/pngdec.h
|
||||||
|
)
|
||||||
|
|
||||||
set(NP_LIBS src/core/libraries/np_manager/np_manager.cpp
|
set(NP_LIBS src/core/libraries/np_manager/np_manager.cpp
|
||||||
src/core/libraries/np_manager/np_manager.h
|
src/core/libraries/np_manager/np_manager.h
|
||||||
src/core/libraries/np_score/np_score.cpp
|
src/core/libraries/np_score/np_score.cpp
|
||||||
|
@ -290,6 +294,7 @@ set(CORE src/core/aerolib/stubs.cpp
|
||||||
${PAD_LIB}
|
${PAD_LIB}
|
||||||
${VIDEOOUT_LIB}
|
${VIDEOOUT_LIB}
|
||||||
${NP_LIBS}
|
${NP_LIBS}
|
||||||
|
${PNG_LIB}
|
||||||
${MISC_LIBS}
|
${MISC_LIBS}
|
||||||
src/core/linker.cpp
|
src/core/linker.cpp
|
||||||
src/core/linker.h
|
src/core/linker.h
|
||||||
|
|
|
@ -10,10 +10,6 @@
|
||||||
|
|
||||||
namespace Audio {
|
namespace Audio {
|
||||||
|
|
||||||
int SDLAudio::AudioInit() {
|
|
||||||
return SDL_InitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
}
|
|
||||||
|
|
||||||
int SDLAudio::AudioOutOpen(int type, u32 samples_num, u32 freq,
|
int SDLAudio::AudioOutOpen(int type, u32 samples_num, u32 freq,
|
||||||
Libraries::AudioOut::OrbisAudioOutParam format) {
|
Libraries::AudioOut::OrbisAudioOutParam format) {
|
||||||
using Libraries::AudioOut::OrbisAudioOutParam;
|
using Libraries::AudioOut::OrbisAudioOutParam;
|
||||||
|
|
|
@ -14,7 +14,6 @@ public:
|
||||||
SDLAudio() = default;
|
SDLAudio() = default;
|
||||||
virtual ~SDLAudio() = default;
|
virtual ~SDLAudio() = default;
|
||||||
|
|
||||||
int AudioInit();
|
|
||||||
int AudioOutOpen(int type, u32 samples_num, u32 freq,
|
int AudioOutOpen(int type, u32 samples_num, u32 freq,
|
||||||
Libraries::AudioOut::OrbisAudioOutParam format);
|
Libraries::AudioOut::OrbisAudioOutParam format);
|
||||||
s32 AudioOutOutput(s32 handle, const void* ptr);
|
s32 AudioOutOutput(s32 handle, const void* ptr);
|
||||||
|
|
|
@ -183,9 +183,8 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!IsOpen()) {
|
if (!IsOpen()) {
|
||||||
const auto ec = std::error_code{errno, std::generic_category()};
|
LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}",
|
||||||
LOG_ERROR(Common_Filesystem, "Failed to open the file at path={}, ec_message={}",
|
PathToUTF8String(file_path));
|
||||||
PathToUTF8String(file_path), ec.message());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ bool ParseFilterRule(Filter& instance, Iterator begin, Iterator end) {
|
||||||
SUB(Lib, AppContent) \
|
SUB(Lib, AppContent) \
|
||||||
SUB(Lib, Rtc) \
|
SUB(Lib, Rtc) \
|
||||||
SUB(Lib, DiscMap) \
|
SUB(Lib, DiscMap) \
|
||||||
|
SUB(Lib, Png) \
|
||||||
CLS(Frontend) \
|
CLS(Frontend) \
|
||||||
CLS(Render) \
|
CLS(Render) \
|
||||||
SUB(Render, Vulkan) \
|
SUB(Render, Vulkan) \
|
||||||
|
|
|
@ -71,6 +71,7 @@ enum class Class : u8 {
|
||||||
Lib_AppContent, ///< The LibSceAppContent implementation.
|
Lib_AppContent, ///< The LibSceAppContent implementation.
|
||||||
Lib_Rtc, ///< The LibSceRtc implementation.
|
Lib_Rtc, ///< The LibSceRtc implementation.
|
||||||
Lib_DiscMap, ///< The LibSceDiscMap implementation.
|
Lib_DiscMap, ///< The LibSceDiscMap implementation.
|
||||||
|
Lib_Png, ///< The LibScePng implementation.
|
||||||
Frontend, ///< Emulator UI
|
Frontend, ///< Emulator UI
|
||||||
Render, ///< Video Core
|
Render, ///< Video Core
|
||||||
Render_Vulkan, ///< Vulkan backend
|
Render_Vulkan, ///< Vulkan backend
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <sstream>
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/path_util.h"
|
#include "common/path_util.h"
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#else
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -215,25 +216,96 @@ enum PosixPageProtection {
|
||||||
|
|
||||||
struct AddressSpace::Impl {
|
struct AddressSpace::Impl {
|
||||||
Impl() {
|
Impl() {
|
||||||
UNREACHABLE();
|
// Allocate virtual address placeholder for our address space.
|
||||||
|
void* hint_address = reinterpret_cast<void*>(SYSTEM_MANAGED_MIN);
|
||||||
|
virtual_size = SystemSize + UserSize;
|
||||||
|
virtual_base = reinterpret_cast<u8*>(
|
||||||
|
mmap(reinterpret_cast<void*>(hint_address), virtual_size, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0));
|
||||||
|
if (virtual_base == MAP_FAILED) {
|
||||||
|
LOG_CRITICAL(Kernel_Vmm, "mmap failed: {}", strerror(errno));
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
}
|
||||||
|
madvise(virtual_base, virtual_size, MADV_HUGEPAGE);
|
||||||
|
|
||||||
|
backing_fd = memfd_create("BackingDmem", 0);
|
||||||
|
if (backing_fd < 0) {
|
||||||
|
LOG_CRITICAL(Kernel_Vmm, "memfd_create failed: {}", strerror(errno));
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defined to extend the file with zeros
|
||||||
|
int ret = ftruncate(backing_fd, BackingSize);
|
||||||
|
if (ret != 0) {
|
||||||
|
LOG_CRITICAL(Kernel_Vmm, "ftruncate failed with {}, are you out-of-memory?",
|
||||||
|
strerror(errno));
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map backing dmem handle.
|
||||||
|
backing_base = static_cast<u8*>(
|
||||||
|
mmap(nullptr, BackingSize, PROT_READ | PROT_WRITE, MAP_SHARED, backing_fd, 0));
|
||||||
|
if (backing_base == MAP_FAILED) {
|
||||||
|
LOG_CRITICAL(Kernel_Vmm, "mmap failed: {}", strerror(errno));
|
||||||
|
throw std::bad_alloc{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const VAddr start_addr = reinterpret_cast<VAddr>(virtual_base);
|
||||||
|
m_free_regions.insert({start_addr, start_addr + virtual_size});
|
||||||
}
|
}
|
||||||
|
|
||||||
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, PosixPageProtection prot) {
|
void* Map(VAddr virtual_addr, PAddr phys_addr, size_t size, PosixPageProtection prot) {
|
||||||
UNREACHABLE();
|
m_free_regions.subtract({virtual_addr, virtual_addr + size});
|
||||||
return nullptr;
|
const int fd = phys_addr != -1 ? backing_fd : -1;
|
||||||
|
const int host_offset = phys_addr != -1 ? phys_addr : 0;
|
||||||
|
const int flag = phys_addr != -1 ? MAP_SHARED : (MAP_ANONYMOUS | MAP_PRIVATE);
|
||||||
|
void* ret = mmap(reinterpret_cast<void*>(virtual_addr), size, prot, MAP_FIXED | flag, fd,
|
||||||
|
host_offset);
|
||||||
|
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Unmap(VAddr virtual_addr, PAddr phys_addr, size_t size) {
|
void Unmap(VAddr virtual_addr, PAddr phys_addr, size_t size) {
|
||||||
UNREACHABLE();
|
// Check to see if we are adjacent to any regions.
|
||||||
|
auto start_address = virtual_addr;
|
||||||
|
auto end_address = start_address + size;
|
||||||
|
auto it = m_free_regions.find({start_address - 1, end_address + 1});
|
||||||
|
|
||||||
|
// If we are, join with them, ensuring we stay in bounds.
|
||||||
|
if (it != m_free_regions.end()) {
|
||||||
|
start_address = std::min(start_address, it->lower());
|
||||||
|
end_address = std::max(end_address, it->upper());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the relevant region.
|
||||||
|
m_free_regions.insert({start_address, end_address});
|
||||||
|
|
||||||
|
// Return the adjusted pointers.
|
||||||
|
void* ret = mmap(reinterpret_cast<void*>(start_address), end_address - start_address,
|
||||||
|
PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
|
||||||
|
ASSERT_MSG(ret != MAP_FAILED, "mmap failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
|
void Protect(VAddr virtual_addr, size_t size, bool read, bool write, bool execute) {
|
||||||
UNREACHABLE();
|
int flags = PROT_NONE;
|
||||||
|
if (read) {
|
||||||
|
flags |= PROT_READ;
|
||||||
|
}
|
||||||
|
if (write) {
|
||||||
|
flags |= PROT_WRITE;
|
||||||
|
}
|
||||||
|
if (execute) {
|
||||||
|
flags |= PROT_EXEC;
|
||||||
|
}
|
||||||
|
int ret = mprotect(reinterpret_cast<void*>(virtual_addr), size, flags);
|
||||||
|
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int backing_fd;
|
||||||
u8* backing_base{};
|
u8* backing_base{};
|
||||||
u8* virtual_base{};
|
u8* virtual_base{};
|
||||||
size_t virtual_size{};
|
size_t virtual_size{};
|
||||||
|
boost::icl::interval_set<VAddr> m_free_regions;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <fstream>
|
|
||||||
#include <zlib-ng.h>
|
#include <zlib-ng.h>
|
||||||
#include "common/io_file.h"
|
#include "common/io_file.h"
|
||||||
#include "pkg.h"
|
#include "core/file_format/pkg.h"
|
||||||
#include "pkg_type.h"
|
#include "core/file_format/pkg_type.h"
|
||||||
|
|
||||||
static void DecompressPFSC(std::span<const char> compressed_data,
|
static void DecompressPFSC(std::span<const char> compressed_data,
|
||||||
std::span<char> decompressed_data) {
|
std::span<char> decompressed_data) {
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdio>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
|
@ -2,11 +2,8 @@
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "common/io_file.h"
|
#include "common/io_file.h"
|
||||||
#include "psf.h"
|
#include "core/file_format/psf.h"
|
||||||
|
|
||||||
PSF::PSF() = default;
|
PSF::PSF() = default;
|
||||||
|
|
||||||
|
|
|
@ -192,9 +192,8 @@ int PS4_SYSV_ABI sceAudioOutGetSystemState() {
|
||||||
|
|
||||||
int PS4_SYSV_ABI sceAudioOutInit() {
|
int PS4_SYSV_ABI sceAudioOutInit() {
|
||||||
audio = std::make_unique<Audio::SDLAudio>();
|
audio = std::make_unique<Audio::SDLAudio>();
|
||||||
u32 result = audio->AudioInit() == 0 ? ORBIS_OK : ORBIS_AUDIO_OUT_ERROR_NOT_INIT;
|
LOG_INFO(Lib_AudioOut, "called");
|
||||||
LOG_INFO(Lib_AudioOut, "AudioInit returned {}", result);
|
return ORBIS_OK;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI sceAudioOutInitIpmiGetSession() {
|
int PS4_SYSV_ABI sceAudioOutInitIpmiGetSession() {
|
||||||
|
|
|
@ -393,6 +393,8 @@ void* createMutex(void* addr) {
|
||||||
if (addr == nullptr || *static_cast<ScePthreadMutex*>(addr) != nullptr) {
|
if (addr == nullptr || *static_cast<ScePthreadMutex*>(addr) != nullptr) {
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
static std::mutex mutex;
|
||||||
|
std::scoped_lock lk{mutex};
|
||||||
auto vaddr = reinterpret_cast<u64>(addr);
|
auto vaddr = reinterpret_cast<u64>(addr);
|
||||||
|
|
||||||
std::string name = fmt::format("mutex{:#x}", vaddr);
|
std::string name = fmt::format("mutex{:#x}", vaddr);
|
||||||
|
@ -464,7 +466,7 @@ int PS4_SYSV_ABI scePthreadMutexattrInit(ScePthreadMutexattr* attr) {
|
||||||
|
|
||||||
int result = pthread_mutexattr_init(&(*attr)->pth_mutex_attr);
|
int result = pthread_mutexattr_init(&(*attr)->pth_mutex_attr);
|
||||||
|
|
||||||
result = (result == 0 ? scePthreadMutexattrSettype(attr, 1) : result);
|
result = (result == 0 ? scePthreadMutexattrSettype(attr, 2) : result);
|
||||||
result = (result == 0 ? scePthreadMutexattrSetprotocol(attr, 0) : result);
|
result = (result == 0 ? scePthreadMutexattrSetprotocol(attr, 0) : result);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
|
@ -677,7 +679,7 @@ int PS4_SYSV_ABI scePthreadCondTimedwait(ScePthreadCond* cond, ScePthreadMutex*
|
||||||
time.tv_nsec = ((usec % 1000000) * 1000);
|
time.tv_nsec = ((usec % 1000000) * 1000);
|
||||||
int result = pthread_cond_timedwait(&(*cond)->cond, &(*mutex)->pth_mutex, &time);
|
int result = pthread_cond_timedwait(&(*cond)->cond, &(*mutex)->pth_mutex, &time);
|
||||||
|
|
||||||
LOG_INFO(Kernel_Pthread, "scePthreadCondTimedwait, result={}", result);
|
// LOG_INFO(Kernel_Pthread, "scePthreadCondTimedwait, result={}", result);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -1016,7 +1018,7 @@ int PS4_SYSV_ABI scePthreadCondSignal(ScePthreadCond* cond) {
|
||||||
|
|
||||||
int result = pthread_cond_signal(&(*cond)->cond);
|
int result = pthread_cond_signal(&(*cond)->cond);
|
||||||
|
|
||||||
LOG_INFO(Kernel_Pthread, "scePthreadCondSignal, result={}", result);
|
// LOG_INFO(Kernel_Pthread, "scePthreadCondSignal, result={}", result);
|
||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -1165,6 +1167,22 @@ int PS4_SYSV_ABI posix_pthread_create_name_np(ScePthread* thread, const ScePthre
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PS4_SYSV_ABI scePthreadOnce(int* once_control, void (*init_routine)(void)) {
|
||||||
|
return pthread_once(reinterpret_cast<pthread_once_t*>(once_control), init_routine);
|
||||||
|
}
|
||||||
|
int PS4_SYSV_ABI posix_pthread_create(ScePthread* thread, const ScePthreadAttr* attr,
|
||||||
|
pthreadEntryFunc start_routine, void* arg) {
|
||||||
|
LOG_INFO(Kernel_Pthread, "posix pthread_create redirect to scePthreadCreate");
|
||||||
|
|
||||||
|
int result = scePthreadCreate(thread, attr, start_routine, arg, "");
|
||||||
|
if (result != 0) {
|
||||||
|
int rt = result > SCE_KERNEL_ERROR_UNKNOWN && result <= SCE_KERNEL_ERROR_ESTOP
|
||||||
|
? result + -SCE_KERNEL_ERROR_UNKNOWN
|
||||||
|
: POSIX_EOTHER;
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
void pthreadSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
void pthreadSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
||||||
LIB_FUNCTION("4+h9EzwKF4I", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetschedpolicy);
|
LIB_FUNCTION("4+h9EzwKF4I", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetschedpolicy);
|
||||||
LIB_FUNCTION("-Wreprtu0Qs", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetdetachstate);
|
LIB_FUNCTION("-Wreprtu0Qs", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetdetachstate);
|
||||||
|
@ -1186,11 +1204,13 @@ void pthreadSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
||||||
LIB_FUNCTION("x1X76arYMxU", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrGet);
|
LIB_FUNCTION("x1X76arYMxU", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrGet);
|
||||||
LIB_FUNCTION("UTXzJbWhhTE", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetstacksize);
|
LIB_FUNCTION("UTXzJbWhhTE", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrSetstacksize);
|
||||||
LIB_FUNCTION("vNe1w4diLCs", "libkernel", 1, "libkernel", 1, 1, __tls_get_addr);
|
LIB_FUNCTION("vNe1w4diLCs", "libkernel", 1, "libkernel", 1, 1, __tls_get_addr);
|
||||||
|
LIB_FUNCTION("OxhIB8LB-PQ", "libkernel", 1, "libkernel", 1, 1, posix_pthread_create);
|
||||||
|
LIB_FUNCTION("OxhIB8LB-PQ", "libScePosix", 1, "libkernel", 1, 1, posix_pthread_create);
|
||||||
LIB_FUNCTION("bt3CTBKmGyI", "libkernel", 1, "libkernel", 1, 1, scePthreadSetaffinity);
|
LIB_FUNCTION("bt3CTBKmGyI", "libkernel", 1, "libkernel", 1, 1, scePthreadSetaffinity);
|
||||||
LIB_FUNCTION("6UgtwV+0zb4", "libkernel", 1, "libkernel", 1, 1, scePthreadCreate);
|
LIB_FUNCTION("6UgtwV+0zb4", "libkernel", 1, "libkernel", 1, 1, scePthreadCreate);
|
||||||
LIB_FUNCTION("T72hz6ffq08", "libkernel", 1, "libkernel", 1, 1, scePthreadYield);
|
LIB_FUNCTION("T72hz6ffq08", "libkernel", 1, "libkernel", 1, 1, scePthreadYield);
|
||||||
LIB_FUNCTION("-quPa4SEJUw", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrGetstack);
|
LIB_FUNCTION("-quPa4SEJUw", "libkernel", 1, "libkernel", 1, 1, scePthreadAttrGetstack);
|
||||||
|
LIB_FUNCTION("14bOACANTBo", "libkernel", 1, "libkernel", 1, 1, scePthreadOnce);
|
||||||
|
|
||||||
// mutex calls
|
// mutex calls
|
||||||
LIB_FUNCTION("cmo1RIYva9o", "libkernel", 1, "libkernel", 1, 1, scePthreadMutexInit);
|
LIB_FUNCTION("cmo1RIYva9o", "libkernel", 1, "libkernel", 1, 1, scePthreadMutexInit);
|
||||||
|
|
|
@ -27,7 +27,7 @@ struct PthreadMutexattrInternal;
|
||||||
struct PthreadCondInternal;
|
struct PthreadCondInternal;
|
||||||
struct PthreadCondAttrInternal;
|
struct PthreadCondAttrInternal;
|
||||||
struct PthreadRwInternal;
|
struct PthreadRwInternal;
|
||||||
struct PthreadRwLockAttrInernal;
|
struct PthreadRwLockAttrInternal;
|
||||||
|
|
||||||
using SceKernelSchedParam = ::sched_param;
|
using SceKernelSchedParam = ::sched_param;
|
||||||
using ScePthread = PthreadInternal*;
|
using ScePthread = PthreadInternal*;
|
||||||
|
@ -37,7 +37,7 @@ using ScePthreadMutexattr = PthreadMutexattrInternal*;
|
||||||
using ScePthreadCond = PthreadCondInternal*;
|
using ScePthreadCond = PthreadCondInternal*;
|
||||||
using ScePthreadCondattr = PthreadCondAttrInternal*;
|
using ScePthreadCondattr = PthreadCondAttrInternal*;
|
||||||
using OrbisPthreadRwlock = PthreadRwInternal*;
|
using OrbisPthreadRwlock = PthreadRwInternal*;
|
||||||
using OrbisPthreadRwlockattr = PthreadRwLockAttrInernal*;
|
using OrbisPthreadRwlockattr = PthreadRwLockAttrInternal*;
|
||||||
|
|
||||||
using pthreadEntryFunc = PS4_SYSV_ABI void* (*)(void*);
|
using pthreadEntryFunc = PS4_SYSV_ABI void* (*)(void*);
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ struct PthreadCondAttrInternal {
|
||||||
pthread_condattr_t cond_attr;
|
pthread_condattr_t cond_attr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PthreadRwLockAttrInernal {
|
struct PthreadRwLockAttrInternal {
|
||||||
u8 reserved[64];
|
u8 reserved[64];
|
||||||
pthread_rwlockattr_t attr_rwlock;
|
pthread_rwlockattr_t attr_rwlock;
|
||||||
int type;
|
int type;
|
||||||
|
|
|
@ -121,7 +121,7 @@ int PS4_SYSV_ABI posix_pthread_rwlockattr_gettype_np() {
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI posix_pthread_rwlockattr_init(OrbisPthreadRwlockattr* attr) {
|
int PS4_SYSV_ABI posix_pthread_rwlockattr_init(OrbisPthreadRwlockattr* attr) {
|
||||||
*attr = new PthreadRwLockAttrInernal{};
|
*attr = new PthreadRwLockAttrInternal{};
|
||||||
int result = pthread_rwlockattr_init(&(*attr)->attr_rwlock);
|
int result = pthread_rwlockattr_init(&(*attr)->attr_rwlock);
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
LOG_ERROR(Kernel_Pthread, "posix_pthread_rwlockattr_init: error = {}", result);
|
LOG_ERROR(Kernel_Pthread, "posix_pthread_rwlockattr_init: error = {}", result);
|
||||||
|
@ -161,7 +161,7 @@ int PS4_SYSV_ABI scePthreadRwlockattrGettype() {
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI scePthreadRwlockattrInit(OrbisPthreadRwlockattr* attr) {
|
int PS4_SYSV_ABI scePthreadRwlockattrInit(OrbisPthreadRwlockattr* attr) {
|
||||||
*attr = new PthreadRwLockAttrInernal{};
|
*attr = new PthreadRwLockAttrInternal{};
|
||||||
int result = pthread_rwlockattr_init(&(*attr)->attr_rwlock);
|
int result = pthread_rwlockattr_init(&(*attr)->attr_rwlock);
|
||||||
if (result != 0) {
|
if (result != 0) {
|
||||||
LOG_ERROR(Kernel_Pthread, "scePthreadRwlockattrInit: error = {}", result);
|
LOG_ERROR(Kernel_Pthread, "scePthreadRwlockattrInit: error = {}", result);
|
||||||
|
|
|
@ -63,9 +63,7 @@ int PS4_SYSV_ABI sceKernelUsleep(u32 microseconds) {
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI posix_usleep(u32 microseconds) {
|
int PS4_SYSV_ABI posix_usleep(u32 microseconds) {
|
||||||
ASSERT(microseconds >= 1000);
|
return sceKernelUsleep(microseconds);
|
||||||
std::this_thread::sleep_for(std::chrono::microseconds(microseconds));
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 PS4_SYSV_ABI sceKernelSleep(u32 seconds) {
|
u32 PS4_SYSV_ABI sceKernelSleep(u32 seconds) {
|
||||||
|
@ -79,11 +77,15 @@ int PS4_SYSV_ABI sceKernelClockGettime(s32 clock_id, OrbisKernelTimespec* tp) {
|
||||||
}
|
}
|
||||||
clockid_t pclock_id = CLOCK_REALTIME;
|
clockid_t pclock_id = CLOCK_REALTIME;
|
||||||
switch (clock_id) {
|
switch (clock_id) {
|
||||||
case 0:
|
case ORBIS_CLOCK_REALTIME:
|
||||||
|
case ORBIS_CLOCK_REALTIME_PRECISE:
|
||||||
|
case ORBIS_CLOCK_REALTIME_FAST:
|
||||||
pclock_id = CLOCK_REALTIME;
|
pclock_id = CLOCK_REALTIME;
|
||||||
break;
|
break;
|
||||||
case 13:
|
case ORBIS_CLOCK_SECOND:
|
||||||
case 4:
|
case ORBIS_CLOCK_MONOTONIC:
|
||||||
|
case ORBIS_CLOCK_MONOTONIC_PRECISE:
|
||||||
|
case ORBIS_CLOCK_MONOTONIC_FAST:
|
||||||
pclock_id = CLOCK_MONOTONIC;
|
pclock_id = CLOCK_MONOTONIC;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -100,7 +102,7 @@ int PS4_SYSV_ABI sceKernelClockGettime(s32 clock_id, OrbisKernelTimespec* tp) {
|
||||||
return SCE_KERNEL_ERROR_EINVAL;
|
return SCE_KERNEL_ERROR_EINVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int PS4_SYSV_ABI clock_gettime(s32 clock_id, OrbisKernelTimespec* time) {
|
int PS4_SYSV_ABI posix_clock_gettime(s32 clock_id, OrbisKernelTimespec* time) {
|
||||||
int result = sceKernelClockGettime(clock_id, time);
|
int result = sceKernelClockGettime(clock_id, time);
|
||||||
if (result < 0) {
|
if (result < 0) {
|
||||||
UNREACHABLE(); // TODO return posix error code
|
UNREACHABLE(); // TODO return posix error code
|
||||||
|
@ -151,6 +153,37 @@ int PS4_SYSV_ABI gettimeofday(OrbisKernelTimeval* tp, OrbisKernelTimezone* tz) {
|
||||||
return sceKernelGettimeofday(tp);
|
return sceKernelGettimeofday(tp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int PS4_SYSV_ABI posix_clock_getres(u32 clock_id, OrbisKernelTimespec* res) {
|
||||||
|
if (res == nullptr) {
|
||||||
|
return SCE_KERNEL_ERROR_EFAULT;
|
||||||
|
}
|
||||||
|
clockid_t pclock_id = CLOCK_REALTIME;
|
||||||
|
switch (clock_id) {
|
||||||
|
case ORBIS_CLOCK_REALTIME:
|
||||||
|
case ORBIS_CLOCK_REALTIME_PRECISE:
|
||||||
|
case ORBIS_CLOCK_REALTIME_FAST:
|
||||||
|
pclock_id = CLOCK_REALTIME;
|
||||||
|
break;
|
||||||
|
case ORBIS_CLOCK_SECOND:
|
||||||
|
case ORBIS_CLOCK_MONOTONIC:
|
||||||
|
case ORBIS_CLOCK_MONOTONIC_PRECISE:
|
||||||
|
case ORBIS_CLOCK_MONOTONIC_FAST:
|
||||||
|
pclock_id = CLOCK_MONOTONIC;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
UNREACHABLE();
|
||||||
|
}
|
||||||
|
|
||||||
|
timespec t{};
|
||||||
|
int result = clock_getres(pclock_id, &t);
|
||||||
|
res->tv_sec = t.tv_sec;
|
||||||
|
res->tv_nsec = t.tv_nsec;
|
||||||
|
if (result == 0) {
|
||||||
|
return SCE_OK;
|
||||||
|
}
|
||||||
|
return SCE_KERNEL_ERROR_EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
void timeSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
void timeSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
||||||
clock = std::make_unique<Common::NativeClock>();
|
clock = std::make_unique<Common::NativeClock>();
|
||||||
initial_ptc = clock->GetUptime();
|
initial_ptc = clock->GetUptime();
|
||||||
|
@ -163,6 +196,7 @@ void timeSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
||||||
LIB_FUNCTION("ejekcaNQNq0", "libkernel", 1, "libkernel", 1, 1, sceKernelGettimeofday);
|
LIB_FUNCTION("ejekcaNQNq0", "libkernel", 1, "libkernel", 1, 1, sceKernelGettimeofday);
|
||||||
LIB_FUNCTION("n88vx3C5nW8", "libkernel", 1, "libkernel", 1, 1, gettimeofday);
|
LIB_FUNCTION("n88vx3C5nW8", "libkernel", 1, "libkernel", 1, 1, gettimeofday);
|
||||||
LIB_FUNCTION("n88vx3C5nW8", "libScePosix", 1, "libkernel", 1, 1, gettimeofday);
|
LIB_FUNCTION("n88vx3C5nW8", "libScePosix", 1, "libkernel", 1, 1, gettimeofday);
|
||||||
|
LIB_FUNCTION("QvsZxomvUHs", "libkernel", 1, "libkernel", 1, 1, sceKernelNanosleep);
|
||||||
LIB_FUNCTION("1jfXLRVzisc", "libkernel", 1, "libkernel", 1, 1, sceKernelUsleep);
|
LIB_FUNCTION("1jfXLRVzisc", "libkernel", 1, "libkernel", 1, 1, sceKernelUsleep);
|
||||||
LIB_FUNCTION("QcteRwbsnV0", "libScePosix", 1, "libkernel", 1, 1, posix_usleep);
|
LIB_FUNCTION("QcteRwbsnV0", "libScePosix", 1, "libkernel", 1, 1, posix_usleep);
|
||||||
LIB_FUNCTION("-ZR+hG7aDHw", "libkernel", 1, "libkernel", 1, 1, sceKernelSleep);
|
LIB_FUNCTION("-ZR+hG7aDHw", "libkernel", 1, "libkernel", 1, 1, sceKernelSleep);
|
||||||
|
@ -170,8 +204,9 @@ void timeSymbolsRegister(Core::Loader::SymbolsResolver* sym) {
|
||||||
LIB_FUNCTION("yS8U2TGCe1A", "libkernel", 1, "libkernel", 1, 1, posix_nanosleep);
|
LIB_FUNCTION("yS8U2TGCe1A", "libkernel", 1, "libkernel", 1, 1, posix_nanosleep);
|
||||||
LIB_FUNCTION("yS8U2TGCe1A", "libScePosix", 1, "libkernel", 1, 1, posix_nanosleep);
|
LIB_FUNCTION("yS8U2TGCe1A", "libScePosix", 1, "libkernel", 1, 1, posix_nanosleep);
|
||||||
LIB_FUNCTION("QBi7HCK03hw", "libkernel", 1, "libkernel", 1, 1, sceKernelClockGettime);
|
LIB_FUNCTION("QBi7HCK03hw", "libkernel", 1, "libkernel", 1, 1, sceKernelClockGettime);
|
||||||
LIB_FUNCTION("lLMT9vJAck0", "libkernel", 1, "libkernel", 1, 1, clock_gettime);
|
LIB_FUNCTION("lLMT9vJAck0", "libkernel", 1, "libkernel", 1, 1, posix_clock_gettime);
|
||||||
LIB_FUNCTION("lLMT9vJAck0", "libScePosix", 1, "libkernel", 1, 1, clock_gettime);
|
LIB_FUNCTION("lLMT9vJAck0", "libScePosix", 1, "libkernel", 1, 1, posix_clock_gettime);
|
||||||
|
LIB_FUNCTION("smIj7eqzZE8", "libScePosix", 1, "libkernel", 1, 1, posix_clock_getres);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Libraries::Kernel
|
} // namespace Libraries::Kernel
|
||||||
|
|
|
@ -26,6 +26,25 @@ struct OrbisKernelTimespec {
|
||||||
s64 tv_nsec;
|
s64 tv_nsec;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
constexpr int ORBIS_CLOCK_REALTIME = 0;
|
||||||
|
constexpr int ORBIS_CLOCK_VIRTUAL = 1;
|
||||||
|
constexpr int ORBIS_CLOCK_PROF = 2;
|
||||||
|
constexpr int ORBIS_CLOCK_MONOTONIC = 4;
|
||||||
|
constexpr int ORBIS_CLOCK_UPTIME = 5;
|
||||||
|
constexpr int ORBIS_CLOCK_UPTIME_PRECISE = 7;
|
||||||
|
constexpr int ORBIS_CLOCK_UPTIME_FAST = 8;
|
||||||
|
constexpr int ORBIS_CLOCK_REALTIME_PRECISE = 9;
|
||||||
|
constexpr int ORBIS_CLOCK_REALTIME_FAST = 10;
|
||||||
|
constexpr int ORBIS_CLOCK_MONOTONIC_PRECISE = 11;
|
||||||
|
constexpr int ORBIS_CLOCK_MONOTONIC_FAST = 12;
|
||||||
|
constexpr int ORBIS_CLOCK_SECOND = 13;
|
||||||
|
constexpr int ORBIS_CLOCK_THREAD_CPUTIME_ID = 14;
|
||||||
|
constexpr int ORBIS_CLOCK_PROCTIME = 15;
|
||||||
|
constexpr int ORBIS_CLOCK_EXT_NETWORK = 16;
|
||||||
|
constexpr int ORBIS_CLOCK_EXT_DEBUG_NETWORK = 17;
|
||||||
|
constexpr int ORBIS_CLOCK_EXT_AD_NETWORK = 18;
|
||||||
|
constexpr int ORBIS_CLOCK_EXT_RAW_NETWORK = 19;
|
||||||
|
|
||||||
u64 PS4_SYSV_ABI sceKernelGetTscFrequency();
|
u64 PS4_SYSV_ABI sceKernelGetTscFrequency();
|
||||||
u64 PS4_SYSV_ABI sceKernelGetProcessTime();
|
u64 PS4_SYSV_ABI sceKernelGetProcessTime();
|
||||||
u64 PS4_SYSV_ABI sceKernelGetProcessTimeCounter();
|
u64 PS4_SYSV_ABI sceKernelGetProcessTimeCounter();
|
||||||
|
|
|
@ -0,0 +1,169 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/libraries/error_codes.h"
|
||||||
|
#include "core/libraries/libs.h"
|
||||||
|
#include "externals/stb_image.h"
|
||||||
|
#include "pngdec.h"
|
||||||
|
|
||||||
|
namespace Libraries::PngDec {
|
||||||
|
|
||||||
|
void setImageInfoParams(OrbisPngDecImageInfo* imageInfo, int width, int height, int channels,
|
||||||
|
bool isInterlaced, bool isTransparent) {
|
||||||
|
if (imageInfo != nullptr) {
|
||||||
|
imageInfo->imageWidth = width;
|
||||||
|
imageInfo->imageHeight = height;
|
||||||
|
imageInfo->bitDepth = 8; // always 8?
|
||||||
|
switch (channels) { // clut missing
|
||||||
|
case 1:
|
||||||
|
imageInfo->colorSpace = OrbisPngDecColorSpace::ORBIS_PNG_DEC_COLOR_SPACE_GRAYSCALE;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
imageInfo->colorSpace =
|
||||||
|
OrbisPngDecColorSpace::ORBIS_PNG_DEC_COLOR_SPACE_GRAYSCALE_ALPHA;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
imageInfo->colorSpace = OrbisPngDecColorSpace::ORBIS_PNG_DEC_COLOR_SPACE_RGB;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
imageInfo->colorSpace = OrbisPngDecColorSpace::ORBIS_PNG_DEC_COLOR_SPACE_RGBA;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
imageInfo->colorSpace = OrbisPngDecColorSpace::ORBIS_PNG_DEC_COLOR_SPACE_RGB;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
imageInfo->imageFlag = 0;
|
||||||
|
if (isInterlaced) {
|
||||||
|
imageInfo->imageFlag |= ORBIS_PNG_DEC_IMAGE_FLAG_ADAM7_INTERLACE;
|
||||||
|
}
|
||||||
|
if (isTransparent) {
|
||||||
|
imageInfo->imageFlag |= ORBIS_PNG_DEC_IMAGE_FLAG_TRNS_CHUNK_EXIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checktRNS(const u8* png_raw, int size) {
|
||||||
|
for (int i = 30; i < size - 4; i += 4) {
|
||||||
|
if (std::memcmp(png_raw + i, "tRNS", 4) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecCreate(const OrbisPngDecCreateParam* param, void* memoryAddress,
|
||||||
|
u32 memorySize, OrbisPngDecHandle* handle) {
|
||||||
|
if (param == nullptr || param->attribute > 1) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid param!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
if (memoryAddress == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid memory address!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_ADDR;
|
||||||
|
}
|
||||||
|
if (param->maxImageWidth - 1 > 1000000) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid size! width = {}", param->maxImageWidth);
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
const OrbisPngDecCreateParam* nextParam = param + 1;
|
||||||
|
int ret = (8 << (reinterpret_cast<uintptr_t>(nextParam) & 0x1f)) *
|
||||||
|
(param->maxImageWidth + 0x47U & 0xfffffff8) +
|
||||||
|
0xd000;
|
||||||
|
*handle = reinterpret_cast<OrbisPngDecHandle>(ret);
|
||||||
|
return ORBIS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDecode(OrbisPngDecHandle handle, const OrbisPngDecDecodeParam* param,
|
||||||
|
OrbisPngDecImageInfo* imageInfo) {
|
||||||
|
if (handle == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "invalid handle!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_HANDLE;
|
||||||
|
}
|
||||||
|
if (param == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid param!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
if (param->pngMemAddr == nullptr || param->pngMemAddr == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "invalid image address!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_ADDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width, height, channels;
|
||||||
|
const u8* png_raw = (const u8*)param->pngMemAddr;
|
||||||
|
u8* img = stbi_load_from_memory(png_raw, param->pngMemSize, &width, &height, &channels,
|
||||||
|
STBI_rgb_alpha); // STBI_rgb_alpha?
|
||||||
|
if (!img) {
|
||||||
|
LOG_ERROR(Lib_Png, "Decoding failed!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_DECODE_ERROR;
|
||||||
|
}
|
||||||
|
bool isInterlaced = (png_raw[28] == 1);
|
||||||
|
bool isTransparent = checktRNS(png_raw, param->pngMemSize);
|
||||||
|
setImageInfoParams(imageInfo, width, height, channels, isInterlaced, isTransparent);
|
||||||
|
u8* imageBuffer = (u8*)(param->imageMemAddr);
|
||||||
|
memcpy(imageBuffer, img, width * height * 4); // copy/pass decoded data
|
||||||
|
stbi_image_free(img);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDecodeWithInputControl() {
|
||||||
|
LOG_ERROR(Lib_Png, "(STUBBED)called");
|
||||||
|
return ORBIS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDelete(OrbisPngDecHandle handle) {
|
||||||
|
handle = nullptr; // ?
|
||||||
|
LOG_ERROR(Lib_Png, "(STUBBED)called");
|
||||||
|
return ORBIS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecParseHeader(const OrbisPngDecParseParam* param,
|
||||||
|
OrbisPngDecImageInfo* imageInfo) {
|
||||||
|
if (param == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid param!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
int width, height, channels;
|
||||||
|
const u8* png_raw = (const u8*)(param->pngMemAddr);
|
||||||
|
int img = stbi_info_from_memory(png_raw, param->pngMemSize, &width, &height, &channels);
|
||||||
|
if (img == 0) {
|
||||||
|
LOG_ERROR(Lib_Png, "Decoding failed!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_DECODE_ERROR;
|
||||||
|
}
|
||||||
|
bool isInterlaced = (png_raw[28] == 1);
|
||||||
|
bool isTransparent = checktRNS(png_raw, param->pngMemSize);
|
||||||
|
setImageInfoParams(imageInfo, width, height, channels, isInterlaced, isTransparent);
|
||||||
|
return ORBIS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecQueryMemorySize(const OrbisPngDecCreateParam* param) {
|
||||||
|
if (param == nullptr) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid param!");
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
if (param->attribute > 1) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid attribute! attribute = {}", param->attribute);
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_ADDR;
|
||||||
|
}
|
||||||
|
if (param->maxImageWidth - 1 > 1000000) {
|
||||||
|
LOG_ERROR(Lib_Png, "Invalid size! width = {}", param->maxImageWidth);
|
||||||
|
return ORBIS_PNG_DEC_ERROR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
int ret =
|
||||||
|
(8 << ((u8)param->attribute & 0x1f)) * (param->maxImageWidth + 0x47U & 0xfffffff8) + 0xd090;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterlibScePngDec(Core::Loader::SymbolsResolver* sym) {
|
||||||
|
LIB_FUNCTION("m0uW+8pFyaw", "libScePngDec", 1, "libScePngDec", 1, 1, scePngDecCreate);
|
||||||
|
LIB_FUNCTION("WC216DD3El4", "libScePngDec", 1, "libScePngDec", 1, 1, scePngDecDecode);
|
||||||
|
LIB_FUNCTION("cJ--1xAbj-I", "libScePngDec", 1, "libScePngDec", 1, 1,
|
||||||
|
scePngDecDecodeWithInputControl);
|
||||||
|
LIB_FUNCTION("QbD+eENEwo8", "libScePngDec", 1, "libScePngDec", 1, 1, scePngDecDelete);
|
||||||
|
LIB_FUNCTION("U6h4e5JRPaQ", "libScePngDec", 1, "libScePngDec", 1, 1, scePngDecParseHeader);
|
||||||
|
LIB_FUNCTION("-6srIGbLTIU", "libScePngDec", 1, "libScePngDec", 1, 1, scePngDecQueryMemorySize);
|
||||||
|
LIB_FUNCTION("cJ--1xAbj-I", "libScePngDec_jvm", 1, "libScePngDec", 1, 1,
|
||||||
|
scePngDecDecodeWithInputControl);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Libraries::PngDec
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/types.h"
|
||||||
|
|
||||||
|
namespace Core::Loader {
|
||||||
|
class SymbolsResolver;
|
||||||
|
}
|
||||||
|
namespace Libraries::PngDec {
|
||||||
|
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_ADDR = 0x80690001;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_SIZE = 0x80690002;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_PARAM = 0x80690003;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_HANDLE = 0x80690004;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_WORK_MEMORY = 0x80690005;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_INVALID_DATA = 0x80690010;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_UNSUPPORT_DATA = 0x80690011;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_DECODE_ERROR = 0x80690012;
|
||||||
|
constexpr int ORBIS_PNG_DEC_ERROR_FATAL = 0x80690020;
|
||||||
|
|
||||||
|
typedef struct OrbisPngDecParseParam {
|
||||||
|
const void* pngMemAddr;
|
||||||
|
u32 pngMemSize;
|
||||||
|
u32 reserved;
|
||||||
|
} OrbisPngDecParseParam;
|
||||||
|
|
||||||
|
typedef struct OrbisPngDecImageInfo {
|
||||||
|
u32 imageWidth;
|
||||||
|
u32 imageHeight;
|
||||||
|
u16 colorSpace;
|
||||||
|
u16 bitDepth;
|
||||||
|
u32 imageFlag;
|
||||||
|
} OrbisPngDecImageInfo;
|
||||||
|
|
||||||
|
typedef enum OrbisPngDecColorSpace {
|
||||||
|
ORBIS_PNG_DEC_COLOR_SPACE_GRAYSCALE = 2,
|
||||||
|
ORBIS_PNG_DEC_COLOR_SPACE_RGB,
|
||||||
|
ORBIS_PNG_DEC_COLOR_SPACE_CLUT,
|
||||||
|
ORBIS_PNG_DEC_COLOR_SPACE_GRAYSCALE_ALPHA = 18,
|
||||||
|
ORBIS_PNG_DEC_COLOR_SPACE_RGBA
|
||||||
|
} ScePngDecColorSpace;
|
||||||
|
|
||||||
|
typedef enum OrbisPngDecImageFlag {
|
||||||
|
ORBIS_PNG_DEC_IMAGE_FLAG_ADAM7_INTERLACE = 1,
|
||||||
|
ORBIS_PNG_DEC_IMAGE_FLAG_TRNS_CHUNK_EXIST = 2
|
||||||
|
} OrbisPngDecImageFlag;
|
||||||
|
|
||||||
|
typedef struct OrbisPngDecCreateParam {
|
||||||
|
u32 thisSize;
|
||||||
|
u32 attribute;
|
||||||
|
u32 maxImageWidth;
|
||||||
|
} OrbisPngDecCreateParam;
|
||||||
|
|
||||||
|
typedef void* OrbisPngDecHandle;
|
||||||
|
|
||||||
|
typedef struct OrbisPngDecDecodeParam {
|
||||||
|
const void* pngMemAddr;
|
||||||
|
void* imageMemAddr;
|
||||||
|
u32 pngMemSize;
|
||||||
|
u32 imageMemSize;
|
||||||
|
u16 pixelFormat;
|
||||||
|
u16 alphaValue;
|
||||||
|
u32 imagePitch;
|
||||||
|
} OrbisPngDecDecodeParam;
|
||||||
|
|
||||||
|
s32 PS4_SYSV_ABI scePngDecCreate(const OrbisPngDecCreateParam* param, void* memoryAddress,
|
||||||
|
u32 memorySize, OrbisPngDecHandle* handle);
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDecode(OrbisPngDecHandle handle, const OrbisPngDecDecodeParam* param,
|
||||||
|
OrbisPngDecImageInfo* imageInfo);
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDecodeWithInputControl();
|
||||||
|
s32 PS4_SYSV_ABI scePngDecDelete(OrbisPngDecHandle handle);
|
||||||
|
s32 PS4_SYSV_ABI scePngDecParseHeader(const OrbisPngDecParseParam* param,
|
||||||
|
OrbisPngDecImageInfo* imageInfo);
|
||||||
|
s32 PS4_SYSV_ABI scePngDecQueryMemorySize(const OrbisPngDecCreateParam* param);
|
||||||
|
|
||||||
|
void RegisterlibScePngDec(Core::Loader::SymbolsResolver* sym);
|
||||||
|
} // namespace Libraries::PngDec
|
|
@ -30,6 +30,7 @@
|
||||||
#include "core/libraries/system/systemservice.h"
|
#include "core/libraries/system/systemservice.h"
|
||||||
#include "core/libraries/system/userservice.h"
|
#include "core/libraries/system/userservice.h"
|
||||||
#include "core/libraries/videoout/video_out.h"
|
#include "core/libraries/videoout/video_out.h"
|
||||||
|
#include "src/core/libraries/libpng/pngdec.h"
|
||||||
|
|
||||||
namespace Libraries {
|
namespace Libraries {
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ void InitHLELibs(Core::Loader::SymbolsResolver* sym) {
|
||||||
Libraries::AppContent::RegisterlibSceAppContent(sym);
|
Libraries::AppContent::RegisterlibSceAppContent(sym);
|
||||||
Libraries::Rtc::RegisterlibSceRtc(sym);
|
Libraries::Rtc::RegisterlibSceRtc(sym);
|
||||||
Libraries::DiscMap::RegisterlibSceDiscMap(sym);
|
Libraries::DiscMap::RegisterlibSceDiscMap(sym);
|
||||||
|
Libraries::PngDec::RegisterlibScePngDec(sym);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Libraries
|
} // namespace Libraries
|
||||||
|
|
124
src/core/tls.cpp
124
src/core/tls.cpp
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <pthread.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
@ -53,9 +55,83 @@ Tcb* GetTcbBase() {
|
||||||
return reinterpret_cast<Tcb*>(TlsGetValue(slot));
|
return reinterpret_cast<Tcb*>(TlsGetValue(slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
static void AllocTcbKey() {
|
||||||
using namespace Xbyak::util;
|
slot = TlsAlloc();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PatchFsAccess(u8* code, const TLSPattern& tls_pattern, Xbyak::CodeGenerator& c) {
|
||||||
|
using namespace Xbyak::util;
|
||||||
|
const auto total_size = tls_pattern.pattern_size + tls_pattern.imm_size;
|
||||||
|
|
||||||
|
// Replace mov instruction with near jump to the trampoline.
|
||||||
|
static constexpr u32 NearJmpSize = 5;
|
||||||
|
auto patch = Xbyak::CodeGenerator(total_size, code);
|
||||||
|
patch.jmp(c.getCurr(), Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||||
|
patch.nop(total_size - NearJmpSize);
|
||||||
|
|
||||||
|
// Write the trampoline.
|
||||||
|
// The following logic is based on the wine implementation of TlsGetValue
|
||||||
|
// https://github.com/wine-mirror/wine/blob/a27b9551/dlls/kernelbase/thread.c#L719
|
||||||
|
static constexpr u32 TlsSlotsOffset = 0x1480;
|
||||||
|
static constexpr u32 TlsExpansionSlotsOffset = 0x1780;
|
||||||
|
static constexpr u32 TlsMinimumAvailable = 64;
|
||||||
|
const u32 teb_offset = slot < TlsMinimumAvailable ? TlsSlotsOffset : TlsExpansionSlotsOffset;
|
||||||
|
const u32 tls_index = slot < TlsMinimumAvailable ? slot : slot - TlsMinimumAvailable;
|
||||||
|
|
||||||
|
const auto target_reg = Xbyak::Reg64(tls_pattern.target_reg);
|
||||||
|
c.mov(target_reg, teb_offset);
|
||||||
|
c.putSeg(gs);
|
||||||
|
c.mov(target_reg, ptr[target_reg]); // Load the pointer to the table of tls slots.
|
||||||
|
c.mov(target_reg,
|
||||||
|
qword[target_reg + tls_index * sizeof(LPVOID)]); // Load the pointer to our buffer.
|
||||||
|
c.jmp(code + total_size); // Return to the instruction right after the mov.
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
static pthread_key_t slot = 0;
|
||||||
|
|
||||||
|
void SetTcbBase(void* image_address) {
|
||||||
|
ASSERT(pthread_setspecific(slot, image_address) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tcb* GetTcbBase() {
|
||||||
|
return reinterpret_cast<Tcb*>(pthread_getspecific(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void AllocTcbKey() {
|
||||||
|
slot = pthread_key_create(&slot, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PatchFsAccess(u8* code, const TLSPattern& tls_pattern, Xbyak::CodeGenerator& c) {
|
||||||
|
using namespace Xbyak::util;
|
||||||
|
const auto total_size = tls_pattern.pattern_size + tls_pattern.imm_size;
|
||||||
|
|
||||||
|
// Replace mov instruction with near jump to the trampoline.
|
||||||
|
static constexpr u32 NearJmpSize = 5;
|
||||||
|
auto patch = Xbyak::CodeGenerator(total_size, code);
|
||||||
|
patch.jmp(c.getCurr(), Xbyak::CodeGenerator::LabelType::T_NEAR);
|
||||||
|
patch.nop(total_size - NearJmpSize);
|
||||||
|
|
||||||
|
// Write the trampoline.
|
||||||
|
// The following logic is based on the glibc implementation of pthread_getspecific
|
||||||
|
// https://github.com/bminor/glibc/blob/29807a27/nptl/pthread_getspecific.c#L23
|
||||||
|
static constexpr u32 PthreadKeySecondLevelSize = 32;
|
||||||
|
static constexpr u32 SpecificFirstBlockOffset = 0x308;
|
||||||
|
static constexpr u32 SelfInTcbheadOffset = 16;
|
||||||
|
static constexpr u32 PthreadKeyDataSize = 16;
|
||||||
|
ASSERT(slot < PthreadKeySecondLevelSize);
|
||||||
|
|
||||||
|
const auto target_reg = Xbyak::Reg64(tls_pattern.target_reg);
|
||||||
|
c.putSeg(fs);
|
||||||
|
c.mov(target_reg, qword[SelfInTcbheadOffset]); // Load self member pointer of tcbhead_t.
|
||||||
|
c.add(target_reg, SpecificFirstBlockOffset + sizeof(uintptr_t) + slot * PthreadKeyDataSize);
|
||||||
|
c.jmp(code + total_size); // Return to the instruction right after the mov.
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
||||||
u8* code = reinterpret_cast<u8*>(segment_addr);
|
u8* code = reinterpret_cast<u8*>(segment_addr);
|
||||||
auto remaining_size = segment_size;
|
auto remaining_size = segment_size;
|
||||||
|
|
||||||
|
@ -89,7 +165,7 @@ void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
||||||
|
|
||||||
// Allocate slot in the process if not done already.
|
// Allocate slot in the process if not done already.
|
||||||
if (slot == 0) {
|
if (slot == 0) {
|
||||||
slot = TlsAlloc();
|
AllocTcbKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace bogus instruction prefix with nops if it exists.
|
// Replace bogus instruction prefix with nops if it exists.
|
||||||
|
@ -98,30 +174,8 @@ void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
||||||
patch.nop(BadPrefix.size());
|
patch.nop(BadPrefix.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace mov instruction with near jump to the trampoline.
|
// Patch access to FS register to a trampoline.
|
||||||
static constexpr u32 NearJmpSize = 5;
|
PatchFsAccess(code, tls_pattern, c);
|
||||||
auto patch = Xbyak::CodeGenerator(total_size, code);
|
|
||||||
patch.jmp(c.getCurr(), Xbyak::CodeGenerator::LabelType::T_NEAR);
|
|
||||||
patch.nop(total_size - NearJmpSize);
|
|
||||||
|
|
||||||
// Write the trampoline.
|
|
||||||
// The following logic is based on the wine implementation of TlsGetValue
|
|
||||||
// https://github.com/wine-mirror/wine/blob/a27b9551/dlls/kernelbase/thread.c#L719
|
|
||||||
static constexpr u32 TlsSlotsOffset = 0x1480;
|
|
||||||
static constexpr u32 TlsExpansionSlotsOffset = 0x1780;
|
|
||||||
static constexpr u32 TlsMinimumAvailable = 64;
|
|
||||||
const u32 teb_offset =
|
|
||||||
slot < TlsMinimumAvailable ? TlsSlotsOffset : TlsExpansionSlotsOffset;
|
|
||||||
const u32 tls_index = slot < TlsMinimumAvailable ? slot : slot - TlsMinimumAvailable;
|
|
||||||
|
|
||||||
const auto target_reg = Xbyak::Reg64(tls_pattern.target_reg);
|
|
||||||
c.mov(target_reg, teb_offset);
|
|
||||||
c.putSeg(gs);
|
|
||||||
c.mov(target_reg, ptr[target_reg]); // Load the pointer to the table of tls slots.
|
|
||||||
c.mov(
|
|
||||||
target_reg,
|
|
||||||
qword[target_reg + tls_index * sizeof(LPVOID)]); // Load the pointer to our buffer.
|
|
||||||
c.jmp(code + total_size); // Return to the instruction right after the mov.
|
|
||||||
|
|
||||||
// Move ahead in module.
|
// Move ahead in module.
|
||||||
code += total_size - 1;
|
code += total_size - 1;
|
||||||
|
@ -133,20 +187,4 @@ void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
|
||||||
|
|
||||||
void SetTcbBase(void* image_address) {
|
|
||||||
UNREACHABLE_MSG("Thread local storage is unimplemented on posix platforms!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Tcb* GetTcbBase() {
|
|
||||||
UNREACHABLE_MSG("Thread local storage is unimplemented on posix platforms!");
|
|
||||||
}
|
|
||||||
|
|
||||||
void PatchTLS(u64 segment_addr, u64 segment_size, Xbyak::CodeGenerator& c) {
|
|
||||||
UNREACHABLE_MSG("Thread local storage is unimplemented on posix platforms!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
|
@ -40,10 +40,6 @@ Emulator::Emulator() : window{WindowWidth, WindowHeight, controller} {
|
||||||
Common::Log::Initialize();
|
Common::Log::Initialize();
|
||||||
Common::Log::Start();
|
Common::Log::Start();
|
||||||
|
|
||||||
// Start discord integration
|
|
||||||
discord_rpc.init();
|
|
||||||
discord_rpc.update(Discord::RPCStatus::Idling, "");
|
|
||||||
|
|
||||||
// Initialize kernel and library facilities.
|
// Initialize kernel and library facilities.
|
||||||
Libraries::Kernel::init_pthreads();
|
Libraries::Kernel::init_pthreads();
|
||||||
Libraries::InitHLELibs(&linker->GetHLESymbols());
|
Libraries::InitHLELibs(&linker->GetHLESymbols());
|
||||||
|
@ -52,7 +48,6 @@ Emulator::Emulator() : window{WindowWidth, WindowHeight, controller} {
|
||||||
Emulator::~Emulator() {
|
Emulator::~Emulator() {
|
||||||
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
|
const auto config_dir = Common::FS::GetUserPath(Common::FS::PathType::UserDir);
|
||||||
Config::save(config_dir / "config.toml");
|
Config::save(config_dir / "config.toml");
|
||||||
discord_rpc.stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Emulator::Run(const std::filesystem::path& file) {
|
void Emulator::Run(const std::filesystem::path& file) {
|
||||||
|
@ -116,7 +111,7 @@ void Emulator::Run(const std::filesystem::path& file) {
|
||||||
std::jthread([this](std::stop_token stop_token) { linker->Execute(); });
|
std::jthread([this](std::stop_token stop_token) { linker->Execute(); });
|
||||||
|
|
||||||
// Begin main window loop until the application exits
|
// Begin main window loop until the application exits
|
||||||
static constexpr std::chrono::microseconds FlipPeriod{100000};
|
static constexpr std::chrono::microseconds FlipPeriod{10};
|
||||||
|
|
||||||
while (window.isOpen()) {
|
while (window.isOpen()) {
|
||||||
window.waitEvent();
|
window.waitEvent();
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include <common/singleton.h>
|
#include <common/singleton.h>
|
||||||
#include "common/discord.h"
|
|
||||||
#include "core/linker.h"
|
#include "core/linker.h"
|
||||||
#include "input/controller.h"
|
#include "input/controller.h"
|
||||||
#include "sdl_window.h"
|
#include "sdl_window.h"
|
||||||
|
@ -23,7 +22,6 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void LoadSystemModules(const std::filesystem::path& file);
|
void LoadSystemModules(const std::filesystem::path& file);
|
||||||
Discord::RPC discord_rpc;
|
|
||||||
Input::GameController* controller = Common::Singleton<Input::GameController>::Instance();
|
Input::GameController* controller = Common::Singleton<Input::GameController>::Instance();
|
||||||
Core::Linker* linker = Common::Singleton<Core::Linker>::Instance();
|
Core::Linker* linker = Common::Singleton<Core::Linker>::Instance();
|
||||||
Frontend::WindowSDL window;
|
Frontend::WindowSDL window;
|
||||||
|
|
|
@ -19,6 +19,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
|
||||||
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||||
UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError());
|
UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
|
||||||
const std::string title = "shadPS4 v" + std::string(Common::VERSION);
|
const std::string title = "shadPS4 v" + std::string(Common::VERSION);
|
||||||
SDL_PropertiesID props = SDL_CreateProperties();
|
SDL_PropertiesID props = SDL_CreateProperties();
|
||||||
|
|
|
@ -695,7 +695,7 @@ struct Liverpool {
|
||||||
NumberFormat NumFormat() const {
|
NumberFormat NumFormat() const {
|
||||||
// There is a small difference between T# and CB number types, account for it.
|
// There is a small difference between T# and CB number types, account for it.
|
||||||
return info.number_type == AmdGpu::NumberFormat::SnormNz ? AmdGpu::NumberFormat::Srgb
|
return info.number_type == AmdGpu::NumberFormat::SnormNz ? AmdGpu::NumberFormat::Srgb
|
||||||
: info.number_type;
|
: info.number_type.Value();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ struct Buffer {
|
||||||
};
|
};
|
||||||
|
|
||||||
u32 GetStride() const noexcept {
|
u32 GetStride() const noexcept {
|
||||||
return stride == 0 ? 1U : stride;
|
return stride == 0 ? 1U : stride.Value();
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 GetStrideElements(u32 element_size) const noexcept {
|
u32 GetStrideElements(u32 element_size) const noexcept {
|
||||||
|
|
Loading…
Reference in New Issue