test(agc): clip and cull distances drawn through the device and the driver
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
04138f50ad
commit
a72dcb93aa
|
|
@ -1277,6 +1277,14 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
# The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex
|
||||
# stage, which must include PA_CL_VS_OUT_CNTL.
|
||||
add_test(NAME agc_driver_vertex_clip_cull_draw_no_key COMMAND agc_driver_vertex_clip_cull_draw_tests no-key)
|
||||
set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_DRAW_KEY=1" SKIP_RETURN_CODE 77)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_vertex_clip_cull_draw_no_key PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
|
||||
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
|
||||
|
|
|
|||
|
|
@ -2248,6 +2248,18 @@ void multisampleTests() {
|
|||
Require(TakeMultisampledClear(clearedCmask, pending, halfFloat, clear) && clear.float32[0] == 1.0f && !TakeMultisampledClear(clearedCmask, pending, halfFloat, clear), "a pending CMASK clear and a whole fill did not clear once to the CMASK clear color");
|
||||
}
|
||||
|
||||
void clipDistanceTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x207] = 0x0040000f;
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).paClVsOutCntl == 0x0040000fu, "DecodeState dropped the PA_CL_VS_OUT_CNTL value the validator quotes");
|
||||
queue.context[0x1b3] = 2;
|
||||
queue.context[0x1b4] = 2;
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "four clip distances were rejected by the precheck");
|
||||
queue.context[0x207] = 0x004001ff;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "more than eight clip and cull distances");
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("more than eight") != std::string::npos, "nine clip distances passed the precheck");
|
||||
}
|
||||
|
||||
void DepthClipTests() {
|
||||
auto queue = makeState();
|
||||
const auto direct = AgcDriver::Graphics::DecodeState(queue);
|
||||
|
|
@ -3356,6 +3368,8 @@ struct ModuleShape {
|
|||
bool fragDepth = false;
|
||||
std::uint32_t sampleMaskLength = 0;
|
||||
std::uint32_t floatControlsWidth = 0;
|
||||
std::uint32_t clipDistanceLength = 0;
|
||||
std::uint32_t cullDistanceLength = 0;
|
||||
};
|
||||
|
||||
void emit(std::vector<std::uint32_t>& out, spv::Op op, std::initializer_list<std::uint32_t> operands) {
|
||||
|
|
@ -3455,6 +3469,30 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|||
emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInSampleMask});
|
||||
extraInterface.push_back(variable);
|
||||
}
|
||||
if (shape.clipDistanceLength != 0) {
|
||||
const auto length = id();
|
||||
const auto array = id();
|
||||
const auto pointer = id();
|
||||
const auto variable = id();
|
||||
emit(declarations, spv::OpConstant, {uintType, length, shape.clipDistanceLength});
|
||||
emit(declarations, spv::OpTypeArray, {array, floatType, length});
|
||||
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array});
|
||||
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput});
|
||||
emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInClipDistance});
|
||||
extraInterface.push_back(variable);
|
||||
}
|
||||
if (shape.cullDistanceLength != 0) {
|
||||
const auto length = id();
|
||||
const auto array = id();
|
||||
const auto pointer = id();
|
||||
const auto variable = id();
|
||||
emit(declarations, spv::OpConstant, {uintType, length, shape.cullDistanceLength});
|
||||
emit(declarations, spv::OpTypeArray, {array, floatType, length});
|
||||
emit(declarations, spv::OpTypePointer, {pointer, spv::StorageClassOutput, array});
|
||||
emit(declarations, spv::OpVariable, {pointer, variable, spv::StorageClassOutput});
|
||||
emit(annotations, spv::OpDecorate, {variable, spv::DecorationBuiltIn, spv::BuiltInCullDistance});
|
||||
extraInterface.push_back(variable);
|
||||
}
|
||||
if (shape.perVertex) {
|
||||
const auto length = id();
|
||||
const auto array = id();
|
||||
|
|
@ -3524,6 +3562,8 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
|
|||
emit(words, spv::OpCapability, {spv::CapabilityShader});
|
||||
if (shape.sampleId) emit(words, spv::OpCapability, {spv::CapabilitySampleRateShading});
|
||||
if (shape.layer) emit(words, spv::OpCapability, {spv::CapabilityGeometry});
|
||||
if (shape.clipDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityClipDistance});
|
||||
if (shape.cullDistanceLength != 0) emit(words, spv::OpCapability, {spv::CapabilityCullDistance});
|
||||
if (shape.barycentric) {
|
||||
emit(words, spv::OpCapability, {spv::CapabilityFragmentBarycentricKHR});
|
||||
const std::string extension = "SPV_KHR_fragment_shader_barycentric";
|
||||
|
|
@ -4814,6 +4854,19 @@ void validationTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, true, false); }, "unsupported device capability 35");
|
||||
pixel.spirv = makeModule({.fragment = true, .sampleMaskLength = 2});
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment built-in");
|
||||
pixel.spirv = makeModule({.fragment = true});
|
||||
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4, .cullDistanceLength = 4});
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true);
|
||||
state.paClVsOutCntl = 0x0040000fu;
|
||||
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 4});
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true); }, "PA_CL_VS_OUT_CNTL=0x0040000f");
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false);
|
||||
vertex.spirv = makeModule({.parameterOutput = true, .cullDistanceLength = 4});
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, false); }, "PA_CL_VS_OUT_CNTL=0x0040000f");
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, false, true);
|
||||
state.paClVsOutCntl = 0;
|
||||
vertex.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 9});
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, false, false, false, false, true, true); }, "clip or cull distance");
|
||||
vertex.spirv = makeModule({.parameterOutput = true, .sampleId = true});
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability 35");
|
||||
vertex.spirv = makeModule({.parameterOutput = true});
|
||||
|
|
@ -5392,6 +5445,7 @@ int main(int argc, char** argv) {
|
|||
uint8x4TargetTests();
|
||||
multisampleTests();
|
||||
MsaaEnableSingleSampleTests();
|
||||
clipDistanceTests();
|
||||
ShaderStageTests();
|
||||
NggVertexWorkgroupTests();
|
||||
TuningFieldTests();
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp"
|
||||
#include "Optimization/SrtWalker.hpp"
|
||||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "SpirvBackend/SpirvAnalysis.hpp"
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
#include "SpirvBackend/SpirvOptimizer.hpp"
|
||||
|
|
@ -1660,6 +1661,22 @@ void verifyMeshConfiguration() {
|
|||
request.graphics = GraphicsCompileContext{0u, {}, mesh, std::nullopt, {}};
|
||||
const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request));
|
||||
require(replay.request.graphics.has_value() && replay.request.graphics->mesh.has_value() && replay.request.graphics->mesh->esgsItemSize == 12u && replay.request.graphics->mesh->primitivesPerGroup == 21u, "mesh configuration was lost in serialization");
|
||||
{
|
||||
RecompileRequest vertex{};
|
||||
vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}};
|
||||
vertex.context.waveSize = 64;
|
||||
ShaderVertexStageInfo info{};
|
||||
info.paClVsOutCntl = 0x0040000fu;
|
||||
vertex.context.vertex = info;
|
||||
const auto back = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(vertex));
|
||||
require(back.request.context.vertex.has_value() && back.request.context.vertex->paClVsOutCntl == 0x0040000fu, "PA_CL_VS_OUT_CNTL was lost in serialization");
|
||||
std::vector<std::uint64_t> clipped;
|
||||
RecompileCacheKey::Build(vertex, clipped);
|
||||
vertex.context.vertex->paClVsOutCntl = 0;
|
||||
std::vector<std::uint64_t> unclipped;
|
||||
RecompileCacheKey::Build(vertex, unclipped);
|
||||
require(clipped != unclipped, "PA_CL_VS_OUT_CNTL is not part of the recompile cache key");
|
||||
}
|
||||
std::vector<std::uint64_t> key;
|
||||
RecompileCacheKey::Build(request, key);
|
||||
const auto first = key;
|
||||
|
|
@ -1809,13 +1826,13 @@ void verifyPixelRequestSerialization() {
|
|||
minimal.context.pixel = ShaderPixelStageInfo{};
|
||||
minimal.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQRMAAAA=", "new requests did not use serialization version 19");
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQRQAAAA=", "new requests did not use serialization version 20");
|
||||
require(serializer.Deserialize(encoded).request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "the pixel primitive class did not round-trip");
|
||||
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
|
||||
for (std::size_t bytes = 0; bytes < 20u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag or primitive class was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRQAAAA="}) {
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQRUAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
static constexpr std::string_view version18Request{
|
||||
|
|
@ -1829,6 +1846,102 @@ void verifyPixelRequestSerialization() {
|
|||
require(version18.request.target.registrationModules && version18.request.target.registrationWordLimit == 1234u && version18.request.target.vertexSubgroups, "a version 18 request trailer was misread");
|
||||
}
|
||||
|
||||
void verifyPositionExportComponents() {
|
||||
constexpr std::uint32_t none = 0xffffffffu;
|
||||
struct Case {
|
||||
std::uint32_t control;
|
||||
std::array<std::uint32_t, 4> clip;
|
||||
std::array<std::uint32_t, 4> cull;
|
||||
};
|
||||
const std::array<Case, 6> cases{{
|
||||
{0x00400000u, {none, none, none, none}, {none, none, none, none}},
|
||||
{0x00400001u, {0u, none, none, none}, {none, none, none, none}},
|
||||
{0x00400002u, {none, 0u, none, none}, {none, none, none, none}},
|
||||
{0x00400005u, {0u, none, 1u, none}, {none, none, none, none}},
|
||||
{0x00400100u, {none, none, none, none}, {0u, none, none, none}},
|
||||
{0x00400400u, {none, none, none, none}, {none, none, 0u, none}},
|
||||
}};
|
||||
for (const auto& test : cases) {
|
||||
for (std::uint32_t component = 0; component < 4; ++component) {
|
||||
const auto output = ShaderRecompiler::DecodePositionExportComponent(test.control, 1u, component);
|
||||
require(output.clipDistance == test.clip[component] && output.cullDistance == test.cull[component], "PA_CL_VS_OUT_CNTL decoded a position component to the wrong clip or cull distance");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> decodeBase64(std::string_view text) {
|
||||
constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::vector<std::uint8_t> bytes;
|
||||
std::uint32_t chunk = 0;
|
||||
std::size_t bits = 0;
|
||||
for (const char symbol : text) {
|
||||
if (symbol == '=') break;
|
||||
const auto value = alphabet.find(symbol);
|
||||
require(value != std::string_view::npos, "invalid base64 symbol");
|
||||
chunk = ((chunk << 6u) | static_cast<std::uint32_t>(value)) & 0xffffffu;
|
||||
bits += 6u;
|
||||
if (bits >= 8u) {
|
||||
bits -= 8u;
|
||||
bytes.push_back(static_cast<std::uint8_t>((chunk >> bits) & 0xffu));
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::string encodeBase64(const std::vector<std::uint8_t>& bytes) {
|
||||
constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string text;
|
||||
for (std::size_t index = 0; index < bytes.size(); index += 3u) {
|
||||
const std::size_t count = std::min<std::size_t>(3u, bytes.size() - index);
|
||||
std::uint32_t chunk = static_cast<std::uint32_t>(bytes[index]) << 16u;
|
||||
if (count > 1u) chunk |= static_cast<std::uint32_t>(bytes[index + 1u]) << 8u;
|
||||
if (count > 2u) chunk |= bytes[index + 2u];
|
||||
for (std::size_t symbol = 0; symbol < 4u; ++symbol) {
|
||||
text.push_back(symbol <= count ? alphabet[(chunk >> (18u - 6u * symbol)) & 0x3fu] : '=');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
void verifyVertexInfoVersion19() {
|
||||
using namespace ShaderRecompiler;
|
||||
const std::array<std::uint32_t, 1> code{0xbf810000u};
|
||||
const RequestSerializer serializer;
|
||||
RecompileRequest vertex{};
|
||||
vertex.shader = {ShaderStage::Vertex, 0x10000u, code, 0, {}};
|
||||
vertex.context.waveSize = 64;
|
||||
vertex.context.vertex = ShaderVertexStageInfo{};
|
||||
const auto unmarked = serializer.Serialize(vertex);
|
||||
vertex.context.vertex->paClVsOutCntl = 0x89abcdefu;
|
||||
const auto marked = serializer.Serialize(vertex);
|
||||
const auto plain = decodeBase64(unmarked);
|
||||
const auto flagged = decodeBase64(marked);
|
||||
require(plain.size() == flagged.size(), "PA_CL_VS_OUT_CNTL changed the request size");
|
||||
std::size_t offset = 0;
|
||||
while (offset < plain.size() && plain[offset] == flagged[offset]) ++offset;
|
||||
require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds");
|
||||
require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it");
|
||||
auto legacy = plain;
|
||||
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
|
||||
for (const auto version : {16u, 17u, 18u, 19u}) {
|
||||
legacy[4] = static_cast<std::uint8_t>(version);
|
||||
const auto replay = serializer.Deserialize(encodeBase64(legacy));
|
||||
require(replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0u, "a legacy vertex info read a PA_CL_VS_OUT_CNTL word");
|
||||
require(serializer.Serialize(replay.request) == unmarked, "a legacy vertex info misread the fields after it");
|
||||
}
|
||||
RecompileRequest pixel{};
|
||||
pixel.shader = {ShaderStage::Fragment, 0x20000u, code, 0, {}};
|
||||
pixel.context.waveSize = 64;
|
||||
pixel.context.pixel = ShaderPixelStageInfo{};
|
||||
pixel.context.pixel->rasterPrimitive = RasterPrimitive::Rectangle;
|
||||
const auto encodedPixel = serializer.Serialize(pixel);
|
||||
auto legacyPixel = decodeBase64(encodedPixel);
|
||||
legacyPixel[4] = 19u;
|
||||
const auto replayPixel = serializer.Deserialize(encodeBase64(legacyPixel));
|
||||
require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle, "a main version 19 request lost its pixel primitive class");
|
||||
require(serializer.Serialize(replayPixel.request) == encodedPixel, "a main version 19 request misread the fields after its pixel primitive class");
|
||||
}
|
||||
|
||||
void verifyLegacyPixelRequests() {
|
||||
using namespace ShaderRecompiler;
|
||||
static constexpr std::array<std::string_view, 14> legacyPixelRequests{
|
||||
|
|
@ -3361,6 +3474,8 @@ int main(int argc, char** argv) {
|
|||
verifyMeshConfiguration();
|
||||
verifyPixelInputs();
|
||||
verifyPixelRequestSerialization();
|
||||
verifyVertexInfoVersion19();
|
||||
verifyPositionExportComponents();
|
||||
verifyLegacyPixelRequests();
|
||||
verifyPixelExportReplay();
|
||||
verifyPixelParameterSlots();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 192;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t DistanceVector = 0x00400000u;
|
||||
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xe0382010, 0x80000405, 0xbf8c3f70,
|
||||
0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
|
||||
0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
struct Vertex {
|
||||
std::array<float, 4> position;
|
||||
std::array<float, 4> distances;
|
||||
};
|
||||
|
||||
constexpr std::array<float, 3> PositionX{-3.0f, 1.0f, 1.0f};
|
||||
constexpr std::array<float, 3> PositionY{-1.0f, -1.0f, 3.0f};
|
||||
constexpr std::array<float, 3> Negative{-1.0f, -1.0f, -1.0f};
|
||||
constexpr std::array<float, 3> Mixed{-1.0f, 1.0f, -1.0f};
|
||||
|
||||
enum class Visible { All, None, X, Y, XAndY };
|
||||
|
||||
struct Row {
|
||||
const char* name;
|
||||
std::uint32_t control;
|
||||
Visible visible;
|
||||
std::array<float, 3> component0;
|
||||
std::array<float, 3> component1;
|
||||
std::array<float, 3> component2;
|
||||
};
|
||||
|
||||
const std::array<Row, 7> Rows{{
|
||||
{"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative},
|
||||
{"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative},
|
||||
{"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative},
|
||||
{"clip planes 0x5, components 0 and 2", DistanceVector | 0x5u, Visible::XAndY, PositionX, Negative, PositionY},
|
||||
{"cull plane 0, every vertex negative", DistanceVector | 0x100u, Visible::None, Negative, Negative, Negative},
|
||||
{"cull plane 0, one vertex not negative", DistanceVector | 0x100u, Visible::All, Mixed, Negative, Negative},
|
||||
{"cull plane 0 from component 2, sparse 0x4", DistanceVector | 0x400u, Visible::None, Mixed, Negative, Negative},
|
||||
}};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
bool Expected(Visible visible, std::uint32_t pixel) {
|
||||
const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width;
|
||||
const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height;
|
||||
switch (visible) {
|
||||
case Visible::All: return true;
|
||||
case Visible::None: return false;
|
||||
case Visible::X: return x >= 0.0;
|
||||
case Visible::Y: return y >= 0.0;
|
||||
case Visible::XAndY: return x >= 0.0 && y >= 0.0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const Row& row) {
|
||||
Pixels.fill(std::byte{0});
|
||||
std::array<Vertex, 3> vertices{};
|
||||
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
||||
vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {row.component0[i], row.component1[i], row.component2[i], 0.0f}};
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast<std::uint32_t>(vertices.size()));
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::ShaderVertexStageInfo vertexInfo{};
|
||||
vertexInfo.paClVsOutCntl = row.control;
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, vertexInfo, vertexMemory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
ShaderRecompiler::ShaderPixelStageInfo pixel{};
|
||||
pixel.wave32 = waveSize == 32u;
|
||||
pixel.targetOutputMode[0] = 9;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
|
||||
{waveSize, 0, std::vector<std::uint32_t>(4, 0u), std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
device.Target(),
|
||||
PixelPushLayout(vertexPush, device.Target())
|
||||
};
|
||||
fragment.useCache = false;
|
||||
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, device.Target())}
|
||||
}};
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.paClVsOutCntl = row.control;
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Width, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blend.colorWriteMask = 15;
|
||||
state.blends = {state.blend};
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(vertices.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Check(const Row& row, std::uint32_t waveSize) {
|
||||
const auto what = std::string("vertex clip and cull distances, ") + row.name + ", wave" + std::to_string(waveSize);
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
const auto value = std::to_integer<std::uint8_t>(Pixels[pixel * 4u]);
|
||||
Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value));
|
||||
const bool written = value == 255u;
|
||||
const bool expected = Expected(row.visible, pixel);
|
||||
Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (device->Target().subgroupSize < 32u) {
|
||||
std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
for (const auto waveSize : {64u, 32u}) {
|
||||
for (const auto& row : Rows) {
|
||||
Draw(*device, waveSize, row);
|
||||
Check(row, waveSize);
|
||||
}
|
||||
}
|
||||
std::puts("vertex clip and cull distance tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Width = 192;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t DistanceVector = 0x00400000u;
|
||||
constexpr std::uint32_t SetContextReg = 0x69u;
|
||||
constexpr std::uint32_t SetShReg = 0x76u;
|
||||
constexpr std::uint32_t SetUconfigReg = 0x79u;
|
||||
constexpr std::uint32_t DrawIndexAuto = 0x2du;
|
||||
|
||||
constexpr std::size_t Block = 65536;
|
||||
constexpr std::size_t ColorBytes = Width * Height * 4;
|
||||
std::byte* ColorMemory = nullptr;
|
||||
alignas(64) volatile std::uint32_t EndOfPipe = 0;
|
||||
|
||||
std::byte* AllocateWatched(std::size_t bytes) {
|
||||
#ifdef _WIN32
|
||||
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
|
||||
#else
|
||||
void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + Block - 1) & ~static_cast<std::uintptr_t>(Block - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast<void*>(aligned + bytes), begin + Block - aligned);
|
||||
void* block = reinterpret_cast<void*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
|
||||
#endif
|
||||
return static_cast<std::byte*>(block);
|
||||
}
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> VertexCode{
|
||||
0xe0382000, 0x80020005, 0xe0382010, 0x80020405, 0xbf8c3f70,
|
||||
0xf80000cf, 0x03020100, 0xf80008df, 0x07060504, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> PixelCode{
|
||||
0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
struct Vertex {
|
||||
std::array<float, 4> position;
|
||||
std::array<float, 4> distances;
|
||||
};
|
||||
|
||||
constexpr std::array<float, 3> PositionX{-3.0f, 1.0f, 1.0f};
|
||||
constexpr std::array<float, 3> PositionY{-1.0f, -1.0f, 3.0f};
|
||||
constexpr std::array<float, 3> Negative{-1.0f, -1.0f, -1.0f};
|
||||
constexpr std::array<float, 3> Mixed{-1.0f, 1.0f, -1.0f};
|
||||
|
||||
enum class Visible { All, None, X, Y };
|
||||
|
||||
struct Step {
|
||||
const char* name;
|
||||
std::uint32_t control;
|
||||
Visible visible;
|
||||
std::array<float, 3> component0;
|
||||
std::array<float, 3> component1;
|
||||
std::array<float, 3> component2;
|
||||
};
|
||||
|
||||
const std::array<Step, 4> Steps{{
|
||||
{"no enable bits", DistanceVector, Visible::All, Negative, Negative, Negative},
|
||||
{"clip plane 0 from component 0", DistanceVector | 0x1u, Visible::X, PositionX, Negative, Negative},
|
||||
{"cull plane 0 from component 2, not every vertex negative", DistanceVector | 0x400u, Visible::All, Negative, Negative, Mixed},
|
||||
{"clip plane 1 alone, from component 1", DistanceVector | 0x2u, Visible::Y, Negative, PositionY, Negative},
|
||||
}};
|
||||
|
||||
struct RegisteredShader {
|
||||
Shader shader;
|
||||
ShaderUserData userData;
|
||||
};
|
||||
RegisteredShader VertexShader{};
|
||||
RegisteredShader PixelShader{};
|
||||
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> ContextBaseline{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0},
|
||||
{0xc, 0},
|
||||
{0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0}, {0x113, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
|
||||
void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) {
|
||||
registered.shader.file_header = 0x34333231u;
|
||||
registered.shader.version = 0x18u;
|
||||
registered.shader.user_data = ®istered.userData;
|
||||
registered.shader.code = code;
|
||||
registered.shader.header_size = sizeof(RegisteredShader);
|
||||
registered.shader.shader_size = bytes;
|
||||
registered.shader.type = type;
|
||||
AgcDriverRegisterShader_nid_postfix(®istered.shader);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
void SetRegister(std::vector<std::uint32_t>& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) {
|
||||
words.push_back(0xc0010000u | (opcode << 8u));
|
||||
words.push_back(offset);
|
||||
words.push_back(value);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Frame(const Step& step, const std::array<Vertex, 3>& vertices) {
|
||||
const auto colorAddress = reinterpret_cast<std::uintptr_t>(ColorMemory);
|
||||
const auto vertexAddress = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixelAddress = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
std::vector<std::uint32_t> words;
|
||||
for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value);
|
||||
SetRegister(words, SetContextReg, 0x207, step.control);
|
||||
SetRegister(words, SetContextReg, 0x318, static_cast<std::uint32_t>(colorAddress >> 8u));
|
||||
SetRegister(words, SetContextReg, 0x390, static_cast<std::uint32_t>(colorAddress >> 40u));
|
||||
SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u));
|
||||
SetRegister(words, SetContextReg, 0x10f, std::bit_cast<std::uint32_t>(Width / 2.0f));
|
||||
SetRegister(words, SetContextReg, 0x110, std::bit_cast<std::uint32_t>(Width / 2.0f));
|
||||
SetRegister(words, SetContextReg, 0x111, std::bit_cast<std::uint32_t>(-(Height / 2.0f)));
|
||||
SetRegister(words, SetContextReg, 0x112, std::bit_cast<std::uint32_t>(Height / 2.0f));
|
||||
const auto extent = (Height << 16u) | Width;
|
||||
SetRegister(words, SetContextReg, 0xd, extent);
|
||||
SetRegister(words, SetContextReg, 0x82, extent);
|
||||
SetRegister(words, SetContextReg, 0x91, extent);
|
||||
SetRegister(words, SetContextReg, 0x95, extent);
|
||||
SetRegister(words, SetUconfigReg, 0x242, 4u);
|
||||
SetRegister(words, SetShReg, 0x008, static_cast<std::uint32_t>(pixelAddress >> 8u));
|
||||
SetRegister(words, SetShReg, 0x009, static_cast<std::uint32_t>(pixelAddress >> 40u));
|
||||
SetRegister(words, SetShReg, 0x00b, 0u);
|
||||
SetRegister(words, SetShReg, 0x0c8, static_cast<std::uint32_t>(vertexAddress >> 8u));
|
||||
SetRegister(words, SetShReg, 0x0c9, static_cast<std::uint32_t>(vertexAddress >> 40u));
|
||||
SetRegister(words, SetShReg, 0x08b, 4u << 1u);
|
||||
const auto descriptor = BufferDescriptor(vertices.data(), sizeof(Vertex), static_cast<std::uint32_t>(vertices.size()));
|
||||
for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]);
|
||||
words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast<std::uint32_t>(vertices.size()), 2u});
|
||||
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&EndOfPipe);
|
||||
words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast<std::uint32_t>(labelAddress), static_cast<std::uint32_t>(static_cast<std::uint64_t>(labelAddress) >> 32u), 1u, 0u, 0u});
|
||||
return words;
|
||||
}
|
||||
|
||||
void Check(const Step& step, const std::vector<std::byte>& colors) {
|
||||
const auto what = std::string("vertex clip and cull distances through the driver, ") + step.name;
|
||||
for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) {
|
||||
const auto value = std::to_integer<std::uint8_t>(colors[pixel * 4u]);
|
||||
Require(value == 0u || value == 255u, what + ": pixel " + std::to_string(pixel) + " stored " + std::to_string(value));
|
||||
const double x = -1.0 + 2.0 * ((pixel % Width) + 0.5) / Width;
|
||||
const double y = 1.0 - 2.0 * ((pixel / Width) + 0.5) / Height;
|
||||
const bool expected = step.visible == Visible::All || (step.visible == Visible::X && x >= 0.0) || (step.visible == Visible::Y && y >= 0.0);
|
||||
const bool written = value == 255u;
|
||||
Require(written == expected, what + ": pixel (" + std::to_string(pixel % Width) + ", " + std::to_string(pixel / Width) + ") is " + (written ? "written" : "clear") + ", expected " + (expected ? "written" : "clear"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A program that exports only pos0 is compiled without the PA_CL_VS_OUT_CNTL position export bits, at
|
||||
// registration and at draw alike; one that exports pos1 keeps them, without the bits outside the layout.
|
||||
void PositionExportLayoutTests() {
|
||||
using AgcDriver::DriverDetail::PositionExportLayout;
|
||||
constexpr std::array<std::uint32_t, 3> PositionOnly{0xf80008cfu, 0x03020100u, 0xbf810000u};
|
||||
Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0040000fu) == 0x0040000fu, "a program exporting pos1 lost its position export bits");
|
||||
Require(PositionExportLayout(nullptr, 0, VertexCode, 0x0144000fu) == 0x0040000fu, "bits outside the position export layout reached the vertex info");
|
||||
Require(PositionExportLayout(nullptr, 0, PositionOnly, 0x0040000fu) == 0u, "a program exporting only pos0 kept the position export bits");
|
||||
AgcDriver::DriverDetail::ShaderSnapshot snapshot{0x10000u, 0u, 1u, {PositionOnly.begin(), PositionOnly.end()}, {}};
|
||||
for (int use = 0; use < 2; ++use) Require(PositionExportLayout(&snapshot, 0, snapshot.code, 0x0040000fu) == 0u, "a cached pos0-only program kept the position export bits");
|
||||
snapshot.code.assign(VertexCode.begin(), VertexCode.end());
|
||||
Require(PositionExportLayout(&snapshot, 2, std::span(snapshot.code).subspan(2), 0x0040000fu) == 0x0040000fu, "a program at an offset was answered from the cache of its snapshot");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
const bool noKey = argc > 1 && std::string(argv[1]) == "no-key";
|
||||
if (noKey) Require(std::getenv("APS5_NO_DRAW_KEY") != nullptr, "the no-key run must start with APS5_NO_DRAW_KEY set");
|
||||
PositionExportLayoutTests();
|
||||
ColorMemory = AllocateWatched(2 * Block);
|
||||
Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
for (const auto& step : Steps) {
|
||||
std::array<Vertex, 3> vertices{};
|
||||
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
||||
vertices[i] = {{PositionX[i], PositionY[i], 0.5f, 1.0f}, {step.component0[i], step.component1[i], step.component2[i], 0.0f}};
|
||||
}
|
||||
std::fill_n(ColorMemory, ColorBytes, std::byte{0});
|
||||
EndOfPipe = 0;
|
||||
auto words = Frame(step, vertices);
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name);
|
||||
std::vector<std::byte> colors(ColorBytes);
|
||||
AgcDriver::GuestMemory::Read(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ColorMemory)), colors);
|
||||
Check(step, colors);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts(noKey ? "vertex clip and cull distance draws passed without the draw key" : "vertex clip and cull distance draws passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue