mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-07-09 17:25:37 -06:00
mac: Migrate to KosmicKrisp (#4566)
This commit is contained in:
parent
8c4e3926bb
commit
3590426ab8
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -233,8 +233,9 @@ jobs:
|
||||
run: |
|
||||
mkdir upload
|
||||
mv ${{github.workspace}}/build/shadps4 upload
|
||||
mv ${{github.workspace}}/build/MoltenVK_icd.json upload
|
||||
mv ${{github.workspace}}/build/libMoltenVK.dylib upload
|
||||
mv ${{github.workspace}}/build/libvulkan.dylib upload
|
||||
mv ${{github.workspace}}/build/libvulkan_kosmickrisp.dylib upload
|
||||
mv ${{github.workspace}}/build/kosmickrisp_mesa_icd.json upload
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: shadps4-macos-sdl-${{ needs.get-info.outputs.date }}-${{ needs.get-info.outputs.shorthash }}
|
||||
|
||||
7
.gitmodules
vendored
7
.gitmodules
vendored
@ -99,10 +99,6 @@
|
||||
url = https://github.com/shadps4-emu/ext-wepoll.git
|
||||
shallow = true
|
||||
branch = dist
|
||||
[submodule "externals/MoltenVK"]
|
||||
path = externals/MoltenVK
|
||||
url = https://github.com/shadPS4-emu/ext-MoltenVK.git
|
||||
shallow = true
|
||||
[submodule "externals/json"]
|
||||
path = externals/json
|
||||
url = https://github.com/nlohmann/json.git
|
||||
@ -151,3 +147,6 @@
|
||||
[submodule "externals/miniupnp"]
|
||||
path = externals/miniupnp
|
||||
url = https://github.com/miniupnp/miniupnp.git
|
||||
[submodule "externals/mesa-kosmickrisp"]
|
||||
path = externals/mesa-kosmickrisp
|
||||
url = https://github.com/shadexternals/mesa-kosmickrisp
|
||||
|
||||
@ -9,8 +9,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
if(APPLE)
|
||||
list(APPEND ADDITIONAL_LANGUAGES OBJC)
|
||||
# Starting with 15.4, Rosetta 2 has support for all the necessary instruction sets.
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 15.4 CACHE STRING "")
|
||||
# Minimum OS version for KosmicKrisp
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 26.0 CACHE STRING "")
|
||||
endif()
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
@ -246,7 +246,7 @@ find_package(SDL3 3.1.2 CONFIG)
|
||||
find_package(stb MODULE)
|
||||
find_package(toml11 4.2.0 CONFIG)
|
||||
find_package(tsl-robin-map 1.3.0 CONFIG)
|
||||
find_package(VulkanHeaders 1.4.329 CONFIG)
|
||||
find_package(VulkanHeaders 1.4.330 CONFIG)
|
||||
find_package(VulkanMemoryAllocator 3.1.0 CONFIG)
|
||||
find_package(xbyak 7.07 CONFIG)
|
||||
find_package(xxHash 0.8.2 MODULE)
|
||||
@ -1216,25 +1216,10 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
# Include MoltenVK, along with an ICD file so it can be found by the system Vulkan loader if used for loading layers.
|
||||
# Bundle Vulkan loader and KosmicKrisp driver with the emulator.
|
||||
set_property(TARGET shadps4 APPEND PROPERTY BUILD_RPATH "@executable_path")
|
||||
set(MVK_DST ${CMAKE_CURRENT_BINARY_DIR})
|
||||
set(MVK_DYLIB_SRC ${CMAKE_CURRENT_BINARY_DIR}/externals/MoltenVK/MoltenVK/libMoltenVK.dylib)
|
||||
set(MVK_DYLIB_DST ${MVK_DST}/libMoltenVK.dylib)
|
||||
set(MVK_ICD_SRC ${CMAKE_CURRENT_SOURCE_DIR}/externals/MoltenVK/MoltenVK/icd/MoltenVK_icd.json)
|
||||
set(MVK_ICD_DST ${MVK_DST}/MoltenVK_icd.json)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${MVK_ICD_DST}
|
||||
DEPENDS ${MVK_ICD_SRC} ${MVK_DST}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${MVK_ICD_SRC} ${MVK_ICD_DST})
|
||||
add_custom_command(
|
||||
OUTPUT ${MVK_DYLIB_DST}
|
||||
DEPENDS ${MVK_DYLIB_SRC} ${MVK_DST}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${MVK_DYLIB_SRC} ${MVK_DYLIB_DST})
|
||||
add_custom_target(CopyMoltenVK DEPENDS ${MVK_ICD_DST} ${MVK_DYLIB_DST})
|
||||
add_dependencies(CopyMoltenVK MoltenVK)
|
||||
add_dependencies(shadps4 CopyMoltenVK)
|
||||
add_subdirectory(externals/mesa-kosmickrisp)
|
||||
AddCopyKosmicKrisp(shadps4 ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
if (ARCHITECTURE STREQUAL "x86_64")
|
||||
# Reserve system-managed memory space.
|
||||
|
||||
@ -76,7 +76,7 @@ Check the build instructions for [**Linux**](https://github.com/shadps4-emu/shad
|
||||
Check the build instructions for [**macOS**](https://github.com/shadps4-emu/shadPS4/blob/main/documents/building-macos.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> macOS users need at least macOS 15.4 to run shadPS4. Due to GPU issues there are currently heavy bugs on Intel Macs.
|
||||
> macOS users need at least macOS 26.0 to run shadPS4. Intel Macs are not supported.
|
||||
|
||||
# Usage examples
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ path = [
|
||||
"documents/Screenshots/*",
|
||||
"documents/Screenshots/Linux/*",
|
||||
"documents/Screenshots/Windows/*",
|
||||
"externals/MoltenVK/MoltenVK_icd.json",
|
||||
"flake.lock",
|
||||
"scripts/ps4_names.txt",
|
||||
"src/images/bronze.png",
|
||||
|
||||
@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
### Install the necessary tools to build shadPS4:
|
||||
|
||||
First, make sure you have **Xcode 16.0 or newer** installed.
|
||||
First, make sure you have **Xcode 26.0 or newer** installed.
|
||||
|
||||
For installing other tools and library dependencies we will be using [Homebrew](https://brew.sh/).
|
||||
|
||||
|
||||
10
externals/CMakeLists.txt
vendored
10
externals/CMakeLists.txt
vendored
@ -260,16 +260,6 @@ if (APPLE OR ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Apple-only dependencies
|
||||
if (APPLE)
|
||||
# MoltenVK
|
||||
if (NOT TARGET MoltenVK)
|
||||
set(MVK_EXCLUDE_SPIRV_TOOLS ON)
|
||||
set(MVK_USE_METAL_PRIVATE_API ON)
|
||||
add_subdirectory(MoltenVK)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
#windows only
|
||||
if (WIN32)
|
||||
add_subdirectory(ext-wepoll)
|
||||
|
||||
1
externals/MoltenVK
vendored
1
externals/MoltenVK
vendored
@ -1 +0,0 @@
|
||||
Subproject commit a14cbf433414aa95da7046694b069f0f3f9263a5
|
||||
1
externals/mesa-kosmickrisp
vendored
Submodule
1
externals/mesa-kosmickrisp
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 801e2de8ef2e3e89fd7ee1da52d67a1294ad6135
|
||||
2
externals/vulkan-headers
vendored
2
externals/vulkan-headers
vendored
@ -1 +1 @@
|
||||
Subproject commit 33d7f512583b8de44d1b6384aa1cf482f92e53e9
|
||||
Subproject commit ee3b5caaa7e372715873c7b9c390ee1c3ca5db25
|
||||
22
src/main.cpp
22
src/main.cpp
@ -9,28 +9,44 @@
|
||||
#include <CLI/CLI.hpp>
|
||||
#include <SDL3/SDL_messagebox.h>
|
||||
|
||||
#include <core/emulator_settings.h>
|
||||
#include <core/emulator_state.h>
|
||||
#include "common/arch.h"
|
||||
#include "common/key_manager.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/memory_patcher.h"
|
||||
#include "common/path_util.h"
|
||||
#include "core/debugger.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/emulator_state.h"
|
||||
#include "core/file_sys/fs.h"
|
||||
#include "core/ipc/ipc.h"
|
||||
#include "core/user_settings.h"
|
||||
#include "emulator.h"
|
||||
#include "imgui/big_picture/big_picture.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <sys/sysctl.h>
|
||||
#endif
|
||||
#include <core/user_settings.h>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && defined(ARCH_X86_64)
|
||||
// KosmicKrisp only supports Apple Silicon. Check that we are not running on an Intel Mac.
|
||||
int sysctl_ret = 0;
|
||||
size_t sysctl_size = sizeof(sysctl_ret);
|
||||
sysctlbyname("sysctl.proc_translated", &sysctl_ret, &sysctl_size, nullptr, 0);
|
||||
if (sysctl_ret != 1) {
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, "shadPS4",
|
||||
"shadPS4 only supports Apple Silicon Macs.", nullptr);
|
||||
std::cout << "shadPS4 only supports Apple Silicon Macs." << std::endl;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
CLI::App app{"shadPS4 Emulator CLI"};
|
||||
|
||||
// ---- CLI state ----
|
||||
|
||||
@ -100,8 +100,7 @@ void Translator::V_INTERP_MOV_F32(const GcnInst& inst) {
|
||||
auto& interp = info.fs_interpolation[attr_index];
|
||||
ASSERT(attr.is_flat || inst.src[0].code == 2);
|
||||
if (profile.supports_amd_shader_explicit_vertex_parameter ||
|
||||
(profile.supports_fragment_shader_barycentric &&
|
||||
!profile.has_incomplete_fragment_shader_barycentric)) {
|
||||
profile.supports_fragment_shader_barycentric) {
|
||||
// VSRC 0=P10, 1=P20, 2=P0
|
||||
interp.primary = Qualifier::PerVertex;
|
||||
SetDst(inst.dst[0],
|
||||
|
||||
@ -44,7 +44,6 @@ struct Profile {
|
||||
bool supports_workgroup_explicit_memory_layout{};
|
||||
bool supports_amd_shader_explicit_vertex_parameter{};
|
||||
bool supports_fragment_shader_barycentric{};
|
||||
bool has_incomplete_fragment_shader_barycentric{};
|
||||
bool has_broken_spirv_clamp{};
|
||||
bool lower_left_origin_mode{};
|
||||
bool needs_manual_interpolation{};
|
||||
|
||||
@ -201,7 +201,6 @@ bool Instance::CreateDevice() {
|
||||
vk::PhysicalDeviceRobustness2FeaturesEXT,
|
||||
vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT,
|
||||
vk::PhysicalDevicePrimitiveTopologyListRestartFeaturesEXT,
|
||||
vk::PhysicalDevicePortabilitySubsetFeaturesKHR,
|
||||
vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT,
|
||||
vk::PhysicalDeviceWorkgroupMemoryExplicitLayoutFeaturesKHR,
|
||||
vk::PhysicalDeviceImage2DViewOf3DFeaturesEXT>();
|
||||
@ -335,21 +334,10 @@ bool Instance::CreateDevice() {
|
||||
LOG_INFO(Render_Vulkan, "- sampler2DViewOf3D: {}",
|
||||
image_2d_view_of_3d_features.sampler2DViewOf3D);
|
||||
}
|
||||
supports_memory_budget = add_extension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
|
||||
const bool calibrated_timestamps =
|
||||
TRACY_GPU_ENABLED ? add_extension(VK_EXT_CALIBRATED_TIMESTAMPS_EXTENSION_NAME) : false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (driver_id == vk::DriverId::eMoltenvk) {
|
||||
portability_subset = add_extension(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
|
||||
if (portability_subset) {
|
||||
portability_features =
|
||||
feature_chain.get<vk::PhysicalDevicePortabilitySubsetFeaturesKHR>();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
supports_memory_budget = add_extension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
|
||||
|
||||
const auto family_properties = physical_device.getQueueFamilyProperties();
|
||||
if (family_properties.empty()) {
|
||||
LOG_CRITICAL(Render_Vulkan, "Physical device reported no queues.");
|
||||
@ -514,27 +502,6 @@ bool Instance::CreateDevice() {
|
||||
.image2DViewOf3D = image_2d_view_of_3d_features.image2DViewOf3D,
|
||||
.sampler2DViewOf3D = image_2d_view_of_3d_features.sampler2DViewOf3D,
|
||||
},
|
||||
#ifdef __APPLE__
|
||||
vk::PhysicalDevicePortabilitySubsetFeaturesKHR{
|
||||
.constantAlphaColorBlendFactors = portability_features.constantAlphaColorBlendFactors,
|
||||
.events = portability_features.events,
|
||||
.imageViewFormatReinterpretation = portability_features.imageViewFormatReinterpretation,
|
||||
.imageViewFormatSwizzle = portability_features.imageViewFormatSwizzle,
|
||||
.imageView2DOn3DImage = portability_features.imageView2DOn3DImage,
|
||||
.multisampleArrayImage = portability_features.multisampleArrayImage,
|
||||
.mutableComparisonSamplers = portability_features.mutableComparisonSamplers,
|
||||
.pointPolygons = portability_features.pointPolygons,
|
||||
.samplerMipLodBias = portability_features.samplerMipLodBias,
|
||||
.separateStencilMaskRef = portability_features.separateStencilMaskRef,
|
||||
.shaderSampleRateInterpolationFunctions =
|
||||
portability_features.shaderSampleRateInterpolationFunctions,
|
||||
.tessellationIsolines = portability_features.tessellationIsolines,
|
||||
.tessellationPointMode = portability_features.tessellationPointMode,
|
||||
.triangleFans = portability_features.triangleFans,
|
||||
.vertexAttributeAccessBeyondStride =
|
||||
portability_features.vertexAttributeAccessBeyondStride,
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
if (!custom_border_color) {
|
||||
|
||||
@ -270,16 +270,6 @@ public:
|
||||
return features.tessellationShader;
|
||||
}
|
||||
|
||||
/// Returns true when tessellation isolines are supported by the device
|
||||
bool IsTessellationIsolinesSupported() const {
|
||||
return !portability_subset || portability_features.tessellationIsolines;
|
||||
}
|
||||
|
||||
/// Returns true when tessellation point mode is supported by the device
|
||||
bool IsTessellationPointModeSupported() const {
|
||||
return !portability_subset || portability_features.tessellationPointMode;
|
||||
}
|
||||
|
||||
/// Returns the vendor ID of the physical device
|
||||
u32 GetVendorID() const {
|
||||
return properties.vendorID;
|
||||
@ -483,7 +473,6 @@ private:
|
||||
vk::PhysicalDeviceFeatures features;
|
||||
vk::PhysicalDeviceVulkan12Features vk12_features;
|
||||
vk::PhysicalDeviceVulkan13Features vk13_features;
|
||||
vk::PhysicalDevicePortabilitySubsetFeaturesKHR portability_features;
|
||||
vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT dynamic_state_3_features;
|
||||
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2_features;
|
||||
vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT shader_atomic_float2_features;
|
||||
@ -523,7 +512,6 @@ private:
|
||||
bool shader_atomic_float{};
|
||||
bool shader_atomic_float2{};
|
||||
bool workgroup_memory_explicit_layout{};
|
||||
bool portability_subset{};
|
||||
bool maintenance_8{};
|
||||
bool attachment_feedback_loop{};
|
||||
bool image_2d_view_of_3d{};
|
||||
|
||||
@ -298,15 +298,12 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_,
|
||||
.supports_amd_shader_explicit_vertex_parameter =
|
||||
instance_.IsAmdShaderExplicitVertexParameterSupported(),
|
||||
.supports_fragment_shader_barycentric = instance_.IsFragmentShaderBarycentricSupported(),
|
||||
.has_incomplete_fragment_shader_barycentric =
|
||||
instance_.IsFragmentShaderBarycentricSupported() &&
|
||||
instance.GetDriverID() == vk::DriverId::eMoltenvk,
|
||||
.needs_manual_interpolation = instance.IsFragmentShaderBarycentricSupported() &&
|
||||
instance.GetDriverID() == vk::DriverId::eNvidiaProprietary,
|
||||
.needs_lds_barriers = instance.GetDriverID() == vk::DriverId::eNvidiaProprietary ||
|
||||
instance.GetDriverID() == vk::DriverId::eMoltenvk,
|
||||
instance.GetDriverID() == vk::DriverId::eMesaKosmickrisp,
|
||||
.needs_buffer_offsets = instance.StorageMinAlignment() > 4,
|
||||
.needs_unorm_fixup = instance.GetDriverID() == vk::DriverId::eMoltenvk,
|
||||
.needs_unorm_fixup = instance.GetDriverID() == vk::DriverId::eMesaKosmickrisp,
|
||||
.needs_clip_distance_emulation = instance.GetDriverID() == vk::DriverId::eNvidiaProprietary,
|
||||
.supports_shader_stencil_export = instance_.IsShaderStencilExportSupported(),
|
||||
};
|
||||
@ -532,9 +529,7 @@ bool PipelineCache::RefreshGraphicsStages() {
|
||||
}
|
||||
break;
|
||||
case AmdGpu::ShaderStageEnable::VgtStages::LsHs:
|
||||
if (!instance.IsTessellationSupported() ||
|
||||
(regs.tess_config.type == AmdGpu::TessellationType::Isoline &&
|
||||
!instance.IsTessellationIsolinesSupported())) {
|
||||
if (!instance.IsTessellationSupported()) {
|
||||
return false;
|
||||
}
|
||||
if (!bind_stage(Stage::Hull, LogicalStage::TessellationControl)) {
|
||||
@ -548,9 +543,7 @@ bool PipelineCache::RefreshGraphicsStages() {
|
||||
}
|
||||
break;
|
||||
case AmdGpu::ShaderStageEnable::VgtStages::LsHsEsGs:
|
||||
if (!instance.IsTessellationSupported() ||
|
||||
(regs.tess_config.type == AmdGpu::TessellationType::Isoline &&
|
||||
!instance.IsTessellationIsolinesSupported())) {
|
||||
if (!instance.IsTessellationSupported()) {
|
||||
return false;
|
||||
}
|
||||
if (!instance.IsGeometryStageSupported()) {
|
||||
|
||||
@ -192,10 +192,6 @@ std::vector<const char*> GetInstanceExtensions(Frontend::WindowSystemType window
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
|
||||
#endif
|
||||
|
||||
if (window_type != Frontend::WindowSystemType::Headless) {
|
||||
extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
|
||||
}
|
||||
@ -262,14 +258,14 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e
|
||||
bool enable_crash_diagnostic) {
|
||||
LOG_INFO(Render_Vulkan, "Creating vulkan instance");
|
||||
|
||||
#if defined(__APPLE__) && !defined(ENABLE_QT_GUI)
|
||||
// Initialize the environment with the path to the MoltenVK ICD, so that the loader will
|
||||
#if defined(__APPLE__)
|
||||
// Initialize the environment with the path to the included ICD, so that the loader will
|
||||
// find it.
|
||||
static const auto icd_path = [] {
|
||||
char path[PATH_MAX];
|
||||
u32 length = PATH_MAX;
|
||||
_NSGetExecutablePath(path, &length);
|
||||
return std::filesystem::path(path).parent_path() / "MoltenVK_icd.json";
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}();
|
||||
setenv("VK_DRIVER_FILES", icd_path.c_str(), true);
|
||||
#endif
|
||||
@ -315,9 +311,6 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e
|
||||
Common::FS::GetUserPathString(Common::FS::PathType::LogDir);
|
||||
const char* log_path = crash_diagnostic_path.c_str();
|
||||
vk::Bool32 enable_force_barriers = vk::True;
|
||||
#ifdef __APPLE__
|
||||
const vk::Bool32 mvk_debug_mode = enable_crash_diagnostic ? vk::True : vk::False;
|
||||
#endif
|
||||
|
||||
const std::array layer_setings = {
|
||||
vk::LayerSettingEXT{
|
||||
@ -404,24 +397,10 @@ vk::UniqueInstance CreateInstance(Frontend::WindowSystemType window_type, bool e
|
||||
.valueCount = 1,
|
||||
.pValues = &enable_force_barriers,
|
||||
},
|
||||
#ifdef __APPLE__
|
||||
// MoltenVK debug mode turns on additional device loss error details, so
|
||||
// use the crash diagnostic setting as an indicator of whether to turn it on.
|
||||
vk::LayerSettingEXT{
|
||||
.pLayerName = "MoltenVK",
|
||||
.pSettingName = "MVK_CONFIG_DEBUG",
|
||||
.type = vk::LayerSettingTypeEXT::eBool32,
|
||||
.valueCount = 1,
|
||||
.pValues = &mvk_debug_mode,
|
||||
},
|
||||
#endif
|
||||
};
|
||||
|
||||
vk::StructureChain<vk::InstanceCreateInfo, vk::LayerSettingsCreateInfoEXT> instance_ci_chain = {
|
||||
vk::InstanceCreateInfo{
|
||||
#ifdef __APPLE__
|
||||
.flags = vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
|
||||
#endif
|
||||
.pApplicationInfo = &application_info,
|
||||
.enabledLayerCount = static_cast<u32>(layers.size()),
|
||||
.ppEnabledLayerNames = layers.data(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user