create vlt device in gnm driver

This commit is contained in:
Asuka 2022-03-29 04:19:58 +08:00
parent a4265a22a9
commit 760068a2b8
16 changed files with 383 additions and 249 deletions

View file

@ -26,7 +26,7 @@ ColumnLimit: 0
SortIncludes: true
IncludeBlocks: Regroup
IncludeCategories:
- Regex: '^"(\.\.|Platform|Emulator|Graphic|Loader|Algorithm|Util)/'
- Regex: '^"(\.\.|Platform|Emulator|Graphic|Loader|Algorithm|Util|Sce|Gnm|Pssl|SpirV|Violet|SceModules)/'
Priority: 2
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
Priority: 3

View file

@ -14,7 +14,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
#define LOG_STR_BUFFER_LEN 2000
#define LOG_STR_BUFFER_LEN 0x2000
#ifdef GPCS4_WINDOWS

View file

@ -119,6 +119,7 @@
<ClInclude Include="Platform\PlatString.h" />
<ClInclude Include="Platform\PlatThread.h" />
<ClInclude Include="Platform\PlatTime.h" />
<ClInclude Include="Platform\PlatVulkan.h" />
<ClInclude Include="SceModules\BlockingQueue.h" />
<ClInclude Include="SceModules\MapSlot.h" />
<ClInclude Include="SceModules\SceAjm\sce_ajm.h" />
@ -276,6 +277,7 @@
<ClCompile Include="Platform\PlatString.cpp" />
<ClCompile Include="Platform\PlatThread.cpp" />
<ClCompile Include="Platform\PlatTime.cpp" />
<ClCompile Include="Platform\PlatVulkan.cpp" />
<ClCompile Include="SceModules\SceAjm\sce_ajm.cpp" />
<ClCompile Include="SceModules\SceAjm\sce_ajm_export.cpp" />
<ClCompile Include="SceModules\SceAppContentUtil\sce_appcontentutil.cpp" />

View file

@ -733,6 +733,9 @@
<ClInclude Include="Graphics\Violet\VltRecycler.h">
<Filter>Source Files\Graphics\Violet</Filter>
</ClInclude>
<ClInclude Include="Platform\PlatVulkan.h">
<Filter>Source Files\Platform</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Loader\EbootObject.cpp">
@ -1263,6 +1266,9 @@
<ClCompile Include="Graphics\Violet\VltDevice.cpp">
<Filter>Source Files\Graphics\Violet</Filter>
</ClCompile>
<ClCompile Include="Platform\PlatVulkan.cpp">
<Filter>Source Files\Platform</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Text Include="Emulator\TLSStub.asm">

View file

@ -4,9 +4,13 @@
#include "UtilMath.h"
#include "sce_errors.h"
#include "Gnm/GnmCommandProcessor.h"
#include "Gnm/GnmCommandBufferDraw.h"
#include "Gnm/GnmCommandBufferDummy.h"
#include "Gnm/GnmCommandProcessor.h"
#include "Violet/VltAdapter.h"
#include "Violet/VltDevice.h"
#include "Violet/VltInstance.h"
LOG_CHANNEL(Graphic.Sce.SceGnmDriver);
@ -14,205 +18,238 @@ extern "C" void glfwPollEvents(void);
namespace sce
{
using namespace vlt;
SceGnmDriver::SceGnmDriver()
{
bool success = initGnmDriver();
LOG_ASSERT(success == true, "init Gnm Driver failed.");
}
SceGnmDriver::~SceGnmDriver()
{
// TODO:
// Correct the reference count maintenance, thus we don't
// need to handle release order manually.
// e.g. wrap VkSurfaceKHR in a class with reference count
m_graphicsQueue.reset();
// Release Presenter before VideoOut
// m_presenter = nullptr;
}
bool SceGnmDriver::initGnmDriver()
{
bool ret = false;
do
SceGnmDriver::SceGnmDriver()
{
// A GPU must have a graphics queue by default.
createGraphicsQueue();
ret = true;
} while (false);
return ret;
}
bool success = initGnmDriver();
LOG_ASSERT(success == true, "init Gnm Driver failed.");
}
int SceGnmDriver::submitCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes)
{
return submitAndFlipCommandBuffers(count,
dcbGpuAddrs, dcbSizesInBytes,
ccbGpuAddrs, ccbSizesInBytes,
0, 0, 0, 0);
}
int SceGnmDriver::submitAndFlipCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes,
uint32_t videoOutHandle,
uint32_t displayBufferIndex,
uint32_t flipMode,
int64_t flipArg)
{
// There's only one hardware graphics queue for most of modern GPUs, including the one on PS4.
// Thus a PS4 game will call submit function to submit command buffers sequentially,
// and normally in one same thread.
// We just emulate the GPU, parsing and executing one command buffer per call.
// TODO:
// For real PS4 system, the submit call is asynchronous.
// Thus for future development, we should record vulkan command buffer asynchronously too,
// reducing time period of the submit call.
LOG_ASSERT(count == 1, "Currently only support 1 cmdbuff at one call.");
SceGpuCommand cmd = {};
cmd.buffer = dcbGpuAddrs[0];
cmd.size = dcbSizesInBytes[0];
m_graphicsQueue->record(cmd);
submitPresent();
return SCE_OK;
}
void SceGnmDriver::submitPresent()
{
do
SceGnmDriver::~SceGnmDriver()
{
//if (!cmdList)
//{
// break;
//}
// TODO:
// Correct the reference count maintenance, thus we don't
// need to handle release order manually.
// e.g. wrap VkSurfaceKHR in a class with reference count
SceGpuSubmission gpuSubmission = {};
//gpuSubmission.cmdList = cmdList;
//gpuSubmission.wait = presentSync.acquire;
//gpuSubmission.wake = presentSync.present;
m_graphicsQueue->submit(gpuSubmission);
m_graphicsQueue.reset();
// Release Presenter before VideoOut
// m_presenter = nullptr;
}
//VltPresentInfo presentation;
//presentation.presenter = m_presenter;
//presentation.waitSync = gpuSubmission.wake;
//m_device->presentImage(presentation);
} while (false);
}
int SceGnmDriver::sceGnmSubmitDone(void)
{
// Gnm::submitDone() is the place to hint the PS4 OS that
// all the currently running GPU tasks (graphics and compute) are done for a frame,
// such that the OS get the permission to do some extra stuffs.
//
// Since we use a window to emulate the hardware display, we need a place
// to process the window event.
// Currently I didn't find a very good place, so I place it here.
glfwPollEvents();
return SCE_OK;
}
void SceGnmDriver::createGraphicsQueue()
{
// Create the only graphics queue.
SceGpuQueueDevice gfxDevice = {};
m_graphicsQueue = std::make_unique<SceGpuQueue>(gfxDevice, SceQueueType::Graphics);
}
uint32_t SceGnmDriver::mapComputeQueue(uint32_t pipeId,
uint32_t queueId,
void* ringBaseAddr,
uint32_t ringSizeInDW,
void* readPtrAddr)
{
int vqueueId = SCE_GNM_ERROR_UNKNOWN;
do
bool SceGnmDriver::initGnmDriver()
{
if (pipeId >= MaxPipeId)
bool ret = false;
do
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_PIPE_ID;
break;
}
if (!initVltDevice())
{
LOG_ERR("init vlt device failed.");
break;
}
if (queueId >= MaxQueueId)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_QUEUE_ID;
break;
}
// A GPU must have a graphics queue by default.
createGraphicsQueue();
ret = true;
} while (false);
return ret;
}
if ((uintptr_t)ringBaseAddr % 256 != 0)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_RING_BASE_ADDR;
break;
}
if (!::util::isPowerOfTwo(ringSizeInDW))
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_RING_SIZE;
break;
}
if ((uintptr_t)readPtrAddr % 4 != 0)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_READ_PTR_ADDR;
break;
}
*(uint32_t*)readPtrAddr = 0;
vqueueId = VQueueIdBegin + pipeId * MaxPipeId + queueId;
if (vqueueId >= MaxComputeQueueCount)
{
LOG_ERR("vqueueId is larger than max queue count.");
break;
}
SceGpuQueueDevice cptDevice = {};
//cptDevice.device = m_device;
//cptDevice.presenter = nullptr;
//cptDevice.videoOut = nullptr;
uint32_t vqueueIndex = vqueueId - VQueueIdBegin;
m_computeQueues[vqueueIndex] = std::make_unique<SceGpuQueue>(cptDevice, SceQueueType::Compute);
} while (false);
return vqueueId;
}
void SceGnmDriver::unmapComputeQueue(uint32_t vqueueId)
{
do
bool SceGnmDriver::initVltDevice()
{
if (vqueueId >= MaxComputeQueueCount)
bool ret = false;
do
{
LOG_ERR("vqueueId is larger than max queue count.");
break;
}
m_instance = new VltInstance();
uint32_t vqueueIndex = vqueueId - VQueueIdBegin;
m_computeQueues[vqueueIndex].reset();
// adapters are ranked internally by their power
// typically first one is the most powerful GPU in system
m_adapter = m_instance->enumAdapters(0);
if (!m_adapter)
{
break;
}
} while (false);
}
m_device = m_adapter->createDevice(m_instance);
if (!m_device)
{
break;
}
void SceGnmDriver::dingDong(
uint32_t vqueueId,
uint32_t nextStartOffsetInDw)
{
}
ret = true;
}while(false);
return ret;
}
int SceGnmDriver::submitCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes)
{
return submitAndFlipCommandBuffers(count,
dcbGpuAddrs, dcbSizesInBytes,
ccbGpuAddrs, ccbSizesInBytes,
0, 0, 0, 0);
}
int SceGnmDriver::submitAndFlipCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes,
uint32_t videoOutHandle,
uint32_t displayBufferIndex,
uint32_t flipMode,
int64_t flipArg)
{
// There's only one hardware graphics queue for most of modern GPUs, including the one on PS4.
// Thus a PS4 game will call submit function to submit command buffers sequentially,
// and normally in one same thread.
// We just emulate the GPU, parsing and executing one command buffer per call.
// TODO:
// For real PS4 system, the submit call is asynchronous.
// Thus for future development, we should record vulkan command buffer asynchronously too,
// reducing time period of the submit call.
LOG_ASSERT(count == 1, "Currently only support 1 cmdbuff at one call.");
SceGpuCommand cmd = {};
cmd.buffer = dcbGpuAddrs[0];
cmd.size = dcbSizesInBytes[0];
m_graphicsQueue->record(cmd);
submitPresent();
return SCE_OK;
}
void SceGnmDriver::submitPresent()
{
do
{
//if (!cmdList)
//{
// break;
//}
SceGpuSubmission gpuSubmission = {};
//gpuSubmission.cmdList = cmdList;
//gpuSubmission.wait = presentSync.acquire;
//gpuSubmission.wake = presentSync.present;
m_graphicsQueue->submit(gpuSubmission);
//VltPresentInfo presentation;
//presentation.presenter = m_presenter;
//presentation.waitSync = gpuSubmission.wake;
//m_device->presentImage(presentation);
} while (false);
}
int SceGnmDriver::sceGnmSubmitDone(void)
{
// Gnm::submitDone() is the place to hint the PS4 OS that
// all the currently running GPU tasks (graphics and compute) are done for a frame,
// such that the OS get the permission to do some extra stuffs.
//
// Since we use a window to emulate the hardware display, we need a place
// to process the window event.
// Currently I didn't find a very good place, so I place it here.
glfwPollEvents();
return SCE_OK;
}
void SceGnmDriver::createGraphicsQueue()
{
// Create the only graphics queue.
SceGpuQueueDevice gfxDevice = {};
m_graphicsQueue = std::make_unique<SceGpuQueue>(gfxDevice, SceQueueType::Graphics);
}
uint32_t SceGnmDriver::mapComputeQueue(uint32_t pipeId,
uint32_t queueId,
void* ringBaseAddr,
uint32_t ringSizeInDW,
void* readPtrAddr)
{
int vqueueId = SCE_GNM_ERROR_UNKNOWN;
do
{
if (pipeId >= MaxPipeId)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_PIPE_ID;
break;
}
if (queueId >= MaxQueueId)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_QUEUE_ID;
break;
}
if ((uintptr_t)ringBaseAddr % 256 != 0)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_RING_BASE_ADDR;
break;
}
if (!::util::isPowerOfTwo(ringSizeInDW))
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_RING_SIZE;
break;
}
if ((uintptr_t)readPtrAddr % 4 != 0)
{
vqueueId = SCE_GNM_ERROR_COMPUTEQUEUE_INVALID_READ_PTR_ADDR;
break;
}
*(uint32_t*)readPtrAddr = 0;
vqueueId = VQueueIdBegin + pipeId * MaxPipeId + queueId;
if (vqueueId >= MaxComputeQueueCount)
{
LOG_ERR("vqueueId is larger than max queue count.");
break;
}
SceGpuQueueDevice cptDevice = {};
//cptDevice.device = m_device;
//cptDevice.presenter = nullptr;
//cptDevice.videoOut = nullptr;
uint32_t vqueueIndex = vqueueId - VQueueIdBegin;
m_computeQueues[vqueueIndex] = std::make_unique<SceGpuQueue>(cptDevice, SceQueueType::Compute);
} while (false);
return vqueueId;
}
void SceGnmDriver::unmapComputeQueue(uint32_t vqueueId)
{
do
{
if (vqueueId >= MaxComputeQueueCount)
{
LOG_ERR("vqueueId is larger than max queue count.");
break;
}
uint32_t vqueueIndex = vqueueId - VQueueIdBegin;
m_computeQueues[vqueueIndex].reset();
} while (false);
}
void SceGnmDriver::dingDong(
uint32_t vqueueId,
uint32_t nextStartOffsetInDw)
{
}
} // namespace sce

View file

@ -2,73 +2,88 @@
#include "SceCommon.h"
#include "Violet/VltRc.h"
#include <array>
#include <memory>
namespace sce
{
class SceGpuQueue;
namespace vlt
{
class VltInstance;
class VltAdapter;
class VltDevice;
} // namespace vlt
// Valid vqueue id should be positive value.
constexpr uint32_t VQueueIdBegin = 1;
constexpr uint32_t MaxPipeId = 7; // Some docs say it should be 7, others say it should be 3. Fuck that.
constexpr uint32_t MaxQueueId = 8;
constexpr uint32_t MaxComputeQueueCount = MaxPipeId * MaxQueueId;
class SceGpuQueue;
class SceGnmDriver
{
// Valid vqueue id should be positive value.
constexpr uint32_t VQueueIdBegin = 1;
constexpr uint32_t MaxPipeId = 7; // Some docs say it should be 7, others say it should be 3. Fuck that.
constexpr uint32_t MaxQueueId = 8;
constexpr uint32_t MaxComputeQueueCount = MaxPipeId * MaxQueueId;
public:
SceGnmDriver();
~SceGnmDriver();
class SceGnmDriver
{
/// Graphics
int submitCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes);
public:
SceGnmDriver();
~SceGnmDriver();
int submitAndFlipCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes,
uint32_t videoOutHandle,
uint32_t displayBufferIndex,
uint32_t flipMode,
int64_t flipArg);
/// Graphics
int sceGnmSubmitDone(void);
int submitCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes);
/// Compute
int submitAndFlipCommandBuffers(uint32_t count,
void* dcbGpuAddrs[],
uint32_t* dcbSizesInBytes,
void* ccbGpuAddrs[],
uint32_t* ccbSizesInBytes,
uint32_t videoOutHandle,
uint32_t displayBufferIndex,
uint32_t flipMode,
int64_t flipArg);
uint32_t mapComputeQueue(
uint32_t pipeId,
uint32_t queueId,
void* ringBaseAddr,
uint32_t ringSizeInDW,
void* readPtrAddr);
int sceGnmSubmitDone(void);
void unmapComputeQueue(uint32_t vqueueId);
/// Compute
void dingDong(
uint32_t vqueueId,
uint32_t nextStartOffsetInDw);
uint32_t mapComputeQueue(
uint32_t pipeId,
uint32_t queueId,
void* ringBaseAddr,
uint32_t ringSizeInDW,
void* readPtrAddr);
private:
bool initGnmDriver();
void unmapComputeQueue(uint32_t vqueueId);
void createGraphicsQueue();
void dingDong(
uint32_t vqueueId,
uint32_t nextStartOffsetInDw);
void submitPresent();
private:
bool initGnmDriver();
private:
std::unique_ptr<SceGpuQueue> m_graphicsQueue;
std::array<std::unique_ptr<SceGpuQueue>, MaxComputeQueueCount> m_computeQueues;
};
bool initVltDevice();
void createGraphicsQueue();
void submitPresent();
private:
vlt::Rc<vlt::VltInstance> m_instance;
vlt::Rc<vlt::VltAdapter> m_adapter;
vlt::Rc<vlt::VltDevice> m_device;
std::unique_ptr<SceGpuQueue> m_graphicsQueue;
std::array<std::unique_ptr<SceGpuQueue>, MaxComputeQueueCount>
m_computeQueues;
};
} // namespace sce

View file

@ -429,14 +429,14 @@ namespace sce::vlt
for (uint32_t family : queueFamiliySet)
{
VkDeviceQueueCreateInfo graphicsQueue;
graphicsQueue.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
graphicsQueue.pNext = nullptr;
graphicsQueue.flags = 0;
graphicsQueue.queueFamilyIndex = family;
graphicsQueue.queueCount = 1;
graphicsQueue.pQueuePriorities = &queuePriority;
queueInfos.push_back(graphicsQueue);
VkDeviceQueueCreateInfo deviceQueue;
deviceQueue.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
deviceQueue.pNext = nullptr;
deviceQueue.flags = 0;
deviceQueue.queueFamilyIndex = family;
deviceQueue.queueCount = 1;
deviceQueue.pQueuePriorities = &queuePriority;
queueInfos.push_back(deviceQueue);
}
VkDeviceCreateInfo info;
@ -458,7 +458,7 @@ namespace sce::vlt
Logger::exception("DxvkAdapter: Failed to create device");
Rc<VltDevice> result = new VltDevice(
instance, this, devExtensions, requestFeatures);
instance, this, device, devExtensions, requestFeatures);
return result;
}
@ -797,6 +797,7 @@ namespace sce::vlt
{
Logger::info(util::str::formatex("Queue families:",
"\n Graphics : ", queues.graphics,
"\n Compute : ", queues.compute,
"\n Transfer : ", queues.transfer));
}

View file

@ -47,7 +47,7 @@ namespace sce::vlt
};
/**
* \brief DXVK adapter
* \brief VLT adapter
*
* Corresponds to a physical device in Vulkan. Provides
* all kinds of information about the device itself and

View file

@ -7,8 +7,10 @@ namespace sce::vlt
VltDevice::VltDevice(
const Rc<VltInstance>& instance,
const Rc<VltAdapter>& adapter,
VkDevice device,
const VltDeviceExtensions& extensions,
const VltDeviceFeatures& features) :
m_device(device),
m_instance(instance),
m_adapter(adapter),
m_extensions(extensions),

View file

@ -45,6 +45,7 @@ namespace sce::vlt
VltDevice(
const Rc<VltInstance>& instance,
const Rc<VltAdapter>& adapter,
VkDevice device,
const VltDeviceExtensions& extensions,
const VltDeviceFeatures& features);

View file

@ -19,7 +19,11 @@ namespace sce::vlt
{
if (properties.apiVersion < VK_MAKE_VERSION(1, 3, 0))
{
Logger::warn(util::str::formatex("Skipping Vulkan 1.3 adapter: ", properties.deviceName));
Logger::warn(util::str::formatex("Skipping Vulkan ",
VK_API_VERSION_MAJOR(properties.apiVersion),
".",
VK_API_VERSION_MINOR(properties.apiVersion),
" adapter: ", properties.deviceName));
return false;
}

View file

@ -1,7 +1,10 @@
#include "VltInstance.h"
#include "VltAdapter.h"
#include "VltDeviceFilter.h"
#include "Platform/PlatVulkan.h"
#include <array>
LOG_CHANNEL(Graphic.Violet);
@ -30,6 +33,19 @@ namespace sce::vlt
: nullptr;
}
std::vector<VltExt> VltInstance::getPlatformExtensions()
{
std::vector<VltExt> result;
auto platformExtensions = plat::vulkanGetRequiredInstanceExtensions();
for (const auto& ext : platformExtensions)
{
result.emplace_back(ext, VltExtMode::Required);
}
return result;
}
VkInstance VltInstance::createInstance()
{
VltInstanceExtensions insExtensions;
@ -39,6 +55,12 @@ namespace sce::vlt
&insExtensions.khrSurface,
} };
auto platformExtensions = getPlatformExtensions();
for (auto& ext : platformExtensions)
{
insExtensionList.push_back(&ext);
}
std::vector<const char*> insLayers;
#ifdef VLT_VALIDATION_AND_DEBUG
@ -56,7 +78,6 @@ namespace sce::vlt
Logger::exception("DxvkInstance: Failed to create instance");
m_extensions = insExtensions;
VltNameList extensionNameList = extensionsEnabled.toNameList();
Logger::info("Enabled instance extensions:");
@ -159,4 +180,5 @@ namespace sce::vlt
for (uint32_t i = 0; i < names.count(); i++)
Logger::info(util::str::formatex(" ", names.name(i)));
}
} // namespace sce::vlt

View file

@ -7,8 +7,9 @@
namespace sce::vlt
{
class VltAdapter;
/**
* \brief DXVK instance
* \brief VLT instance
*
* Manages a Vulkan instance and stores a list
* of adapters. This also provides methods for
@ -62,6 +63,8 @@ namespace sce::vlt
}
private:
std::vector<VltExt> getPlatformExtensions();
VkInstance createInstance();
std::vector<Rc<VltAdapter>> queryAdapters();

View file

@ -173,6 +173,10 @@ namespace sce::vlt
{
return m_object != nullptr;
}
operator bool() const
{
return m_object != nullptr;
}
private:
T* m_object = nullptr;

View file

@ -0,0 +1,25 @@
#include "PlatVulkan.h"
#ifdef GPCS4_WINDOWS
// this acctually should be done using glfwGetRequiredInstanceExtensions
// but due to initialize order issue, we implement it here
std::vector<const char*> plat::vulkanGetRequiredInstanceExtensions()
{
// clang-format off
return std::vector<const char*>
({
"VK_KHR_win32_surface"
});
// clang-format on
}
#else
std::vector<const char*> plat::vulkanGetRequiredInstanceExtensions()
{
// TODO:
}
#endif

View file

@ -0,0 +1,12 @@
#pragma once
#include "GPCS4Common.h"
#include <vector>
namespace plat
{
std::vector<const char*> vulkanGetRequiredInstanceExtensions();
} // namespace plat