mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge pull request #9685 from thuanquang/fix/linear-target-completion-stores
fix(agc): store resident linear color targets at completion points, limited to their drawn rectangles
This commit is contained in:
18 files changed
+3034
-53
No files matched your search
@@ -2181,6 +2181,12 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_address_passes_draw_transitions COMMAND agc_driver_address_passes_tests --targets)
|
||||
set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1;APS5_DRAW_TRANSITIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_address_passes_draw_transitions PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_target_properties(agc_driver_rcp_hardware_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests)
|
||||
set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
@@ -2225,6 +2231,7 @@ endif()
|
||||
|
||||
target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver)
|
||||
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
|
||||
target_link_libraries(agc_driver_linear_target_completion_points_tests PRIVATE libkernel)
|
||||
|
||||
target_link_libraries(agc_driver_storage_file_write_tests PRIVATE libkernel)
|
||||
|
||||
@@ -2239,6 +2246,16 @@ target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $<T
|
||||
target_include_directories(agc_driver_draw_cache_relocation_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_linear_target_completion_points_sync_flip COMMAND agc_driver_linear_target_completion_points_tests --flip)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_sync_flip PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SYNC_FLIP=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_driver_linear_target_completion_points_unpipelined COMMAND agc_driver_linear_target_completion_points_tests --label --interrupt)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_unpipelined PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_linear_target_completion_points_sync_flip agc_driver_linear_target_completion_points_unpipelined PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_submit_progress COMMAND agc_driver_submit_progress_tests)
|
||||
set_tests_properties(agc_submit_progress PROPERTIES
|
||||
ENVIRONMENT "APS5_SUBMIT_PROGRESS=1;ANYPS5_REQUIRE_VULKAN=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -394,6 +395,7 @@ void Driver::execute(const Submission& submission) {
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
if (localDevice != nullptr) static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("flip"));
|
||||
|
||||
if (!recordLabelsForPacket(localDevice.get(), submission.queue) && localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
|
||||
if (localDevice != nullptr) {
|
||||
|
||||
@@ -38,11 +38,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
recordDeferredLabels(localDevice.get(), submission.queue);
|
||||
const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
int reason = localDevice != nullptr ? 0 : 1;
|
||||
if (label.has_value()) {
|
||||
const auto bytes = label->Bytes();
|
||||
const auto stamp = ++eventSerial;
|
||||
if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true;
|
||||
reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(label->address, bytes, stamp, submission.queue) : 4;
|
||||
if (reason == 1) GuestMemory::Write(label->address, bytes, 4);
|
||||
if (reason == 0 || reason == 1 || reason == 5) {
|
||||
@@ -52,6 +53,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
countLabelOutcome(reason);
|
||||
++immediateLabels;
|
||||
} else {
|
||||
if (localDevice != nullptr && submission.queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true;
|
||||
if (localDevice != nullptr) Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before interrupt");
|
||||
++noOpLabels;
|
||||
}
|
||||
@@ -249,6 +251,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
||||
|
||||
recordDeferredLabels(draining.get(), submission.queue);
|
||||
if (draining != nullptr) {
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("drain"));
|
||||
if (unlockedDrain && draining->CanWaitUnlocked()) epoch = draining->SubmitAndEpoch();
|
||||
else draining->WaitIdle();
|
||||
}
|
||||
@@ -437,10 +440,11 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
const auto localDevice = device.Load();
|
||||
recordDeferredLabels(localDevice.get(), queue);
|
||||
const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
|
||||
int reason = localDevice != nullptr ? 0 : 1;
|
||||
if (!bytes.empty()) {
|
||||
const auto stamp = ++eventSerial;
|
||||
if (localDevice != nullptr && queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("label") != 0) workOpen = true;
|
||||
reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(address, bytes, stamp, queue) : 4;
|
||||
if (reason != 0 && reason != 5 && reason != 6) {
|
||||
if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle();
|
||||
@@ -455,6 +459,7 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span<c
|
||||
}
|
||||
if (endOfPipeInterrupt) {
|
||||
bool deferred = false;
|
||||
if (bytes.empty() && localDevice != nullptr && queue == 0 && Graphics::StorageTexture::StoreDrawnLinear("interrupt") != 0) workOpen = true;
|
||||
if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) {
|
||||
deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0);
|
||||
if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0);
|
||||
|
||||
@@ -2362,6 +2362,7 @@ void VulkanDevice::PrepareForReplacement() {
|
||||
|
||||
void VulkanDevice::WaitIdle() {
|
||||
APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle begin");
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("wait idle"));
|
||||
RetirePresents(0);
|
||||
if (state->recorder) {
|
||||
// Announced with this call's return address: the [recorder] site table then names the
|
||||
@@ -2468,6 +2469,7 @@ bool VulkanDevice::StoresPendingOver(std::uint64_t address, std::size_t bytes) c
|
||||
int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst) {
|
||||
static const bool storeAtLabel = Graphics::StorageTexture::StoreAtLabelRequested(std::getenv("APS5_STORE_AT_LABEL"));
|
||||
if (storeAtLabel) Graphics::StorageTexture::FlushAllPending("label");
|
||||
if (queue == 0) static_cast<void>(Graphics::StorageTexture::StoreDrawnLinear("label"));
|
||||
Graphics::StorageTexture::StoreReadbackBeforeLabel("readback before label");
|
||||
if (!state->recorder || bytes.empty() || bytes.size() > 65536 || address % 4 != 0 || bytes.size() % 4 != 0) return 4;
|
||||
auto& recorder = *state->recorder;
|
||||
@@ -5021,6 +5023,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
};
|
||||
if (recorder.HasQueuedKeyStores() && (resources.HoldsLease() || recorder.AnyQueuedKeyStore(touches))) recorder.FlushKeyStores();
|
||||
if (recorder.HasQueuedStores() && (resources.HoldsLease() || recorder.AnyQueuedStore(touches))) recorder.FlushStores();
|
||||
static_cast<void>(Graphics::StorageTexture::StoreDrawnLinearTouching("dispatch access", touches, resources.HoldsLease()));
|
||||
VkAccessFlags covered = 0;
|
||||
const auto commands = recorder.Commands(&covered);
|
||||
recordStep(PhaseRecordCommands);
|
||||
|
||||
@@ -183,6 +183,7 @@ public:
|
||||
// Whether the OPEN (unsubmitted) batch writes the range: a wait on such a range must submit it.
|
||||
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
|
||||
std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes) const;
|
||||
std::uint64_t PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span<const std::uint64_t> exceptNotes) const;
|
||||
// What SyncThrough(address, bytes) would wait for, for the [hooksync] attribution: the newest
|
||||
// batch whose noted range overlaps the access (the open batch counts first, with the serial it
|
||||
// will get), whether that batch's fence has already signaled (its GPU work is done, so the
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
@@ -204,6 +205,7 @@ public:
|
||||
bool BlockUnits() const { return blockUnits; }
|
||||
std::uint32_t TrackedUnits() const { return trackedLayers; }
|
||||
void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1);
|
||||
bool MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor);
|
||||
void Flush();
|
||||
bool Dirty() const;
|
||||
// Stores every pending image overlapping the range, except `except`; returns whether any was.
|
||||
@@ -220,6 +222,8 @@ public:
|
||||
static constexpr std::uint64_t SmallStoreBytes = 64 * 1024;
|
||||
static std::size_t StoreSmallPending(const char* reason);
|
||||
static void StoreReadbackBeforeLabel(const char* reason);
|
||||
static std::size_t StoreDrawnLinear(const char* reason);
|
||||
static std::size_t StoreDrawnLinearTouching(const char* reason, const std::function<bool(std::uint64_t, std::uint64_t)>& touches, bool all);
|
||||
static bool AnyPending();
|
||||
static void FlushDumpSelected(const char* reason);
|
||||
static bool StoreAtFlipRequested(const char* value);
|
||||
@@ -482,6 +486,27 @@ private:
|
||||
// (`shadowed` receives those guest ranges, `imported` the pieces written to the import).
|
||||
std::uint64_t uploadWindows(const HostImport& import, std::span<const std::pair<std::uint64_t, std::uint64_t>> runs, bool discard = false);
|
||||
std::uint64_t writeBackWindows(std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep, std::uint64_t firstStored, std::uint64_t lastStored, std::vector<ShadowedRange>& shadowed, std::vector<std::pair<std::uint64_t, std::uint64_t>>& imported, Buffer* save = nullptr);
|
||||
struct DrawnRect {
|
||||
std::uint32_t left = 0;
|
||||
std::uint32_t top = 0;
|
||||
std::uint32_t right = 0;
|
||||
std::uint32_t bottom = 0;
|
||||
};
|
||||
struct DrawnArea {
|
||||
bool drawn = false;
|
||||
bool rect = false;
|
||||
std::uint32_t left = 0;
|
||||
std::uint32_t top = 0;
|
||||
std::uint32_t right = 0;
|
||||
std::uint32_t bottom = 0;
|
||||
std::vector<DrawnRect> rects;
|
||||
};
|
||||
DrawnArea drawnArea() const;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> drawnSpans(const DrawnArea& area, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep) const;
|
||||
std::uint64_t storeDrawnRect(const DrawnArea& area, std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> stores, const std::vector<std::uint32_t>& saveUnits);
|
||||
void addDrawnRect(DrawnRect rect);
|
||||
void forgetDrawn();
|
||||
void noteStore(std::uint64_t note);
|
||||
// Per-layer validity (thin array surfaces whose layers are 64 KiB-aligned guest slices; every
|
||||
// other surface is one tracked layer): a layer's generation is the write generation its guest
|
||||
// bytes were last known to match the image at, and a pending layer holds results (a clear, a
|
||||
@@ -518,6 +543,7 @@ private:
|
||||
// Per 64 KiB tracker block of the surface (block 0 holds the base address): the generation of
|
||||
// the tracked layer it belongs to, for GuestMemory::ChangedBlocks.
|
||||
void blockGenerations(std::vector<std::uint64_t>& generations) const;
|
||||
std::uint64_t unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const;
|
||||
// Another image of exactly this surface under another storage format with results pending
|
||||
// (block units, uncompressed, still cached), if any: Refresh takes its pending units from its
|
||||
// image on the device (borrowUnits) instead of having them stored and re-read, and the alias
|
||||
@@ -602,6 +628,7 @@ private:
|
||||
std::uint32_t trackedLayers = 1;
|
||||
std::uint64_t trackedLayerBytes = 0;
|
||||
std::vector<std::uint64_t> layerGeneration;
|
||||
std::vector<std::uint64_t> completionKept;
|
||||
std::vector<bool> layerPending;
|
||||
std::vector<bool> layerBehind;
|
||||
bool blockUnits = false;
|
||||
@@ -641,6 +668,12 @@ private:
|
||||
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
|
||||
// Results are on the GPU only (guarded by the pending-write registry lock).
|
||||
bool dirty = false;
|
||||
bool drawnListed = false;
|
||||
bool drawnWhole = false;
|
||||
bool drawingTarget = false;
|
||||
bool pendingFromOthers = false;
|
||||
std::vector<DrawnRect> drawnRects;
|
||||
std::vector<std::uint64_t> storeNotes;
|
||||
std::uint64_t version = 0;
|
||||
std::uint64_t writes = 0;
|
||||
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
|
||||
|
||||
@@ -1921,7 +1921,7 @@ struct Kept {
|
||||
// The completion side of a recorded draw: the kept objects, the record check, the lease outcome,
|
||||
// the GPU write notes, the copied-buffer write-back (listed in DrawCopiedWriters or run as a
|
||||
// completion action) and the targets marked dirty.
|
||||
void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>& resources, std::shared_ptr<Pipeline> pipeline, std::shared_ptr<Framebuffer> framebuffer, DrawInputs& inputs, std::vector<std::shared_ptr<StorageTexture>> targets, std::vector<std::pair<std::uint32_t, std::uint32_t>> slices, std::unique_ptr<DeviceBuffer> scratch, std::function<void()> checkRecords, bool listed, bool completion, const DrawOutcome& outcome) {
|
||||
void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>& resources, std::shared_ptr<Pipeline> pipeline, std::shared_ptr<Framebuffer> framebuffer, DrawInputs& inputs, std::vector<std::shared_ptr<StorageTexture>> targets, std::vector<std::pair<std::uint32_t, std::uint32_t>> slices, VkRect2D scissor, std::unique_ptr<DeviceBuffer> scratch, std::function<void()> checkRecords, bool listed, bool completion, const DrawOutcome& outcome) {
|
||||
auto kept = std::make_shared<Kept>();
|
||||
kept->resources = resources;
|
||||
kept->pipeline = std::move(pipeline);
|
||||
@@ -1961,14 +1961,15 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>
|
||||
if (resources->Reusable()) recorder.OnComplete([resources] { resources->CheckFaults(); });
|
||||
else recorder.OnComplete([resources] { resources->WriteBackBuffers(); });
|
||||
}
|
||||
std::vector<bool> storeNow(residents.size(), false);
|
||||
for (std::size_t index = 0; index < residents.size(); ++index) {
|
||||
if (residents[index] == nullptr) continue;
|
||||
if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second);
|
||||
if (residents[index]->Descriptor().tileMode == TextureTileMode::kLinear) storeNow[index] = !residents[index]->MarkDrawn(index < slices.size() ? slices[index].first : 0u, index < slices.size() ? slices[index].second : 0u, scissor) || !residents[index]->Cached();
|
||||
else if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index].first, slices[index].second);
|
||||
else residents[index]->MarkDirty();
|
||||
}
|
||||
for (const auto& resident : residents) {
|
||||
if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue;
|
||||
StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast<std::size_t>(resident->GuestBytes()), nullptr, "linear target");
|
||||
for (std::size_t index = 0; index < residents.size(); ++index) {
|
||||
if (storeNow[index]) StorageTexture::FlushPending(residents[index]->Descriptor().baseAddress, static_cast<std::size_t>(residents[index]->GuestBytes()), nullptr, "linear target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2195,7 +2196,6 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
recorder->FlushStores();
|
||||
outcome.storesFlushed = true;
|
||||
}
|
||||
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
|
||||
const bool capture = CaptureInputsEnabled();
|
||||
const bool meshIndirect = state.stages.mesh && args != nullptr;
|
||||
const bool addressBased = resources.HoldsLease();
|
||||
@@ -2217,6 +2217,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
}
|
||||
for (const auto image : record.multisampleImages) passAttachments.emplace_back(image, true);
|
||||
const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites();
|
||||
static_cast<void>(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return std::any_of(passReads.begin(), passReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }) || resources.WritesOverlap(begin, static_cast<std::size_t>(end - begin)); }, unknownMemory));
|
||||
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
|
||||
static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr;
|
||||
const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly();
|
||||
outcome.addressDraw = addressBased;
|
||||
@@ -2361,7 +2363,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
}
|
||||
recorder->LeaveRenderPassOpen(passKey, drawTiming, fenced, passAccess, std::move(storeProxies));
|
||||
timer.phase(PhaseRecord);
|
||||
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
|
||||
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(record.targetSlices), state.scissor, std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
|
||||
timer.phase(PhaseKeep);
|
||||
if (record.waited) {
|
||||
// The wait the dispatch path makes for such work (source 3, "address-based"): the
|
||||
@@ -2952,6 +2954,14 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
report(outcome.waited ? (outcome.reason == SyncLease ? " recorded then waited (lease)" : " recorded then waited (copied writes)") : completion ? " recorded with completion" : " recorded");
|
||||
return;
|
||||
}
|
||||
const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu;
|
||||
const auto touches = [&](std::uint64_t begin, std::uint64_t end) {
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true;
|
||||
if (!gpuIndirect) return false;
|
||||
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
|
||||
};
|
||||
static_cast<void>(StorageTexture::StoreDrawnLinearTouching("draw access", [&](std::uint64_t begin, std::uint64_t end) { return touches(begin, end) || std::any_of(inputs.inPlaceReads.begin(), inputs.inPlaceReads.end(), [&](const auto& read) { return read.first < end && begin < read.second; }); }, resources->HoldsLease() || resources->UsesAddressTable() || resources->HasCopiedWrites()));
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Creating CommandBatch");
|
||||
std::optional<CommandBatch> batch;
|
||||
if (!recorded) {
|
||||
@@ -2961,16 +2971,9 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
}
|
||||
using CommandClass = Recorder::CommandClass;
|
||||
const auto countBarrier = [&](std::uint32_t count = 1) { if (recorded) Recorder::CountBarriers(CommandClass::Draw, count); };
|
||||
const bool gpuIndirect = args != nullptr && indirect.path == IndirectDrawPath::Gpu;
|
||||
// A queued DCC key store over memory the draw writes or reads in place (unknown for an
|
||||
// address-based build), or over its GPU-side records, lands before it, as before a dispatch
|
||||
// (VulkanDevice::dispatch).
|
||||
const auto touches = [&](std::uint64_t begin, std::uint64_t end) {
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
if (resources->WritesOverlap(begin, bytes) || resources->ReadsOverlap(begin, bytes)) return true;
|
||||
if (!gpuIndirect) return false;
|
||||
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
|
||||
};
|
||||
if (recorded && recorder->HasQueuedKeyStores() && (resources->HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores();
|
||||
// A queued label store over such memory likewise (Recorder::RecordStore).
|
||||
if (recorded && recorder->HasQueuedStores() && (resources->HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores();
|
||||
@@ -3137,7 +3140,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
|
||||
APS5_LOG_CHARS_OUT_DEBUG("SubmitAndWait");
|
||||
timer.phase(PhaseRecord);
|
||||
if (recorded) {
|
||||
keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector<std::pair<std::uint32_t, std::uint32_t>> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), std::move(scratch), std::move(checkRecords), listed, completion, outcome);
|
||||
keepRecordedDraw(*recorder, resources, pipeline, framebuffer, inputs, owners, [&] { std::vector<std::pair<std::uint32_t, std::uint32_t>> slices; for (const auto& binding : targets) slices.emplace_back(binding.color.arraySlice, binding.color.arrayLastSlice != 0 ? binding.color.layers : 1u); return slices; }(), state.scissor, std::move(scratch), std::move(checkRecords), listed, completion, outcome);
|
||||
timer.phase(PhaseKeep);
|
||||
if (outcome.waited) {
|
||||
// The wait the dispatch path makes for such work (source 3, "address-based"): the
|
||||
|
||||
@@ -3134,6 +3134,26 @@ std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t b
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::uint64_t Recorder::PendingWriterSerial(std::uint64_t address, std::size_t bytes, std::span<const std::uint64_t> exceptNotes) const {
|
||||
if (exceptNotes.empty()) return PendingWriterSerial(address, bytes);
|
||||
if (bytes == 0) return 0;
|
||||
const auto end = address + bytes;
|
||||
const auto writes = [&](const Batch& batch) {
|
||||
for (std::size_t index = 0; index < batch.writes.size(); ++index) {
|
||||
const auto& [begin, finish] = batch.writes[index];
|
||||
if (address >= finish || begin >= end) continue;
|
||||
if (index < batch.writeNotes.size() && std::find(exceptNotes.begin(), exceptNotes.end(), batch.writeNotes[index]) != exceptNotes.end()) continue;
|
||||
return true;
|
||||
}
|
||||
return readOverlap(batch, address, end, ReadKind::AddressBased).has_value();
|
||||
};
|
||||
if (open != nullptr && writes(*open)) return OpenSerial();
|
||||
for (auto it = inFlight.rbegin(); it != inFlight.rend(); ++it) {
|
||||
if (writes(**it)) return (*it)->serial;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Recorder::signaled(const Batch& batch) const {
|
||||
return batch.submitted && fenceStatus(batch.fence) == VK_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ struct StorageTraffic {
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> writeBacks;
|
||||
std::array<std::pair<std::uint64_t, std::uint64_t>, 4> uploads{};
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> uploadReasons;
|
||||
std::map<std::string, std::pair<std::uint64_t, std::uint64_t>> drawnStores;
|
||||
std::uint64_t directWriteBacks = 0;
|
||||
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
|
||||
};
|
||||
@@ -132,6 +133,14 @@ StorageTraffic& Traffic() {
|
||||
return traffic;
|
||||
}
|
||||
|
||||
struct DrawnPending {
|
||||
std::size_t images = 0;
|
||||
std::uint64_t bytes = 0;
|
||||
double oldestMs = 0;
|
||||
};
|
||||
|
||||
DrawnPending drawnPending();
|
||||
|
||||
void reportStorageTraffic(StorageTraffic& traffic) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - traffic.lastReport < std::chrono::seconds(10)) return;
|
||||
@@ -154,12 +163,19 @@ void reportStorageTraffic(StorageTraffic& traffic) {
|
||||
std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast<unsigned long long>(totals.first), totals.second / 1048576.0);
|
||||
line += " " + reason + text;
|
||||
}
|
||||
std::string drawnLine;
|
||||
for (const auto& [reason, totals] : traffic.drawnStores) {
|
||||
std::snprintf(text, sizeof(text), " %llu/%.1f", static_cast<unsigned long long>(totals.first), totals.second / 1048576.0);
|
||||
drawnLine += " " + reason + text;
|
||||
}
|
||||
const auto drawn = drawnPending();
|
||||
const auto take = [](std::atomic<std::uint64_t>& counter) { return static_cast<unsigned long long>(counter.exchange(0, std::memory_order_relaxed)); };
|
||||
const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks);
|
||||
const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0;
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast<unsigned long long>(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted));
|
||||
AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu; drawn linear targets: stores by reason (count/MiB):%s, pending %zu images / %.1f MiB, oldest %.1f ms\n", line.c_str(), static_cast<unsigned long long>(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted), drawnLine.c_str(), drawn.images, drawn.bytes / 1048576.0, drawn.oldestMs);
|
||||
traffic.writeBacks.clear();
|
||||
traffic.uploadReasons.clear();
|
||||
traffic.drawnStores.clear();
|
||||
traffic.directWriteBacks = 0;
|
||||
}
|
||||
|
||||
@@ -175,7 +191,7 @@ void countStorageUpload(std::size_t path, std::uint64_t bytes) {
|
||||
reportStorageTraffic(traffic);
|
||||
}
|
||||
|
||||
void countStorageWriteBack(std::uint64_t bytes, bool direct) {
|
||||
void countStorageWriteBack(std::uint64_t bytes, bool direct, bool drawn = false) {
|
||||
if (!LookupOutcomes::Profiled()) return;
|
||||
auto& traffic = Traffic();
|
||||
std::lock_guard lock(traffic.mutex);
|
||||
@@ -183,6 +199,11 @@ void countStorageWriteBack(std::uint64_t bytes, bool direct) {
|
||||
++totals.first;
|
||||
totals.second += bytes;
|
||||
if (direct) ++traffic.directWriteBacks;
|
||||
if (drawn) {
|
||||
auto& stores = traffic.drawnStores[flushReason != nullptr ? flushReason : "other"];
|
||||
++stores.first;
|
||||
stores.second += bytes;
|
||||
}
|
||||
reportStorageTraffic(traffic);
|
||||
}
|
||||
|
||||
@@ -1538,6 +1559,13 @@ struct PendingWrites {
|
||||
// Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged).
|
||||
std::vector<StorageTexture*> flushing;
|
||||
std::atomic<std::size_t> flushingCount{0};
|
||||
struct Drawn {
|
||||
StorageTexture* texture;
|
||||
std::uint64_t bytes;
|
||||
std::chrono::steady_clock::time_point since;
|
||||
};
|
||||
std::vector<Drawn> drawn;
|
||||
std::atomic<std::size_t> drawnCount{0};
|
||||
};
|
||||
|
||||
PendingWrites& Pending() {
|
||||
@@ -1545,6 +1573,20 @@ PendingWrites& Pending() {
|
||||
return pending;
|
||||
}
|
||||
|
||||
DrawnPending drawnPending() {
|
||||
DrawnPending result;
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0) return result;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
result.images = pending.drawn.size();
|
||||
for (const auto& entry : pending.drawn) {
|
||||
result.bytes += entry.bytes;
|
||||
result.oldestMs = std::max(result.oldestMs, std::chrono::duration<double, std::milli>(now - entry.since).count());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// See StorageTexture::PendingSerial: bumped after every mutation of the registry above.
|
||||
std::atomic<std::uint64_t> pendingSerial{0};
|
||||
|
||||
@@ -3804,7 +3846,13 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count)
|
||||
}
|
||||
}
|
||||
if (anyUnsaved && !GuestMemory::Watched(descriptor.baseAddress, static_cast<std::size_t>(guestBytes))) captureGuestBytes(&unsaved);
|
||||
for (std::uint32_t layer = first; layer < first + count; ++layer) layerPending[layer] = true;
|
||||
for (std::uint32_t layer = first; layer < first + count; ++layer) {
|
||||
if (!layerPending[layer] && !completionKept.empty()) {
|
||||
const auto end = layerBegin(layer) + layerBytes(layer);
|
||||
for (auto at = layerBegin(layer) & ~std::uint64_t{65535}; at < end; at += 65536) completionKept[static_cast<std::size_t>(at / 65536 - descriptor.baseAddress / 65536)] = 0;
|
||||
}
|
||||
layerPending[layer] = true;
|
||||
}
|
||||
// Results of this image now cover the alias's results borrowed into these units: this image
|
||||
// stores them, the alias no longer has to (its results, unchanged since the borrow, are in
|
||||
// this image's content). An alias written since keeps its own newer results pending.
|
||||
@@ -3827,6 +3875,10 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count)
|
||||
}
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!drawingTarget) {
|
||||
pendingFromOthers = true;
|
||||
if (drawnListed) drawnWhole = true;
|
||||
}
|
||||
// The active recorder installs a hook that also waits for recorded work; without one (tests),
|
||||
// pending storage results alone are flushed.
|
||||
if (Recorder::Active() == nullptr) {
|
||||
@@ -3851,6 +3903,7 @@ void StorageTexture::reconcilePending() {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
const bool any = anyLayerPending();
|
||||
if (!any) forgetDrawn();
|
||||
if (any == dirty) return;
|
||||
dirty = any;
|
||||
if (any) pending.textures.push_back(this);
|
||||
@@ -3864,6 +3917,215 @@ bool StorageTexture::Dirty() const {
|
||||
return dirty;
|
||||
}
|
||||
|
||||
constexpr std::size_t MaxDrawnRects = 16;
|
||||
|
||||
bool StorageTexture::MarkDrawn(std::uint32_t slice, std::uint32_t count, VkRect2D scissor) {
|
||||
struct Drawing {
|
||||
bool& flag;
|
||||
bool previous;
|
||||
~Drawing() { flag = previous; }
|
||||
} drawing{drawingTarget, std::exchange(drawingTarget, true)};
|
||||
MarkSliceDirty(slice, count);
|
||||
const auto clip = [](std::int64_t value, std::uint32_t limit) { return static_cast<std::uint32_t>(std::clamp<std::int64_t>(value, 0, limit)); };
|
||||
const auto left = clip(scissor.offset.x, descriptor.width);
|
||||
const auto top = clip(scissor.offset.y, descriptor.height);
|
||||
const auto right = clip(std::int64_t{scissor.offset.x} + scissor.extent.width, descriptor.width);
|
||||
const auto bottom = clip(std::int64_t{scissor.offset.y} + scissor.extent.height, descriptor.height);
|
||||
const bool rectangle = proxyImage == VK_NULL_HANDLE && descriptor.tileMode == TextureTileMode::kLinear && !blockUnits && trackedLayers == 1 && descriptor.mipCount == 1 && arrayLayers == 1 && geometry.imageLayers == 1 && geometry.imageDepth == 1 && !geometry.thick && descriptor.dccAddress == 0;
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!anyLayerPending()) return true;
|
||||
if (!drawnListed) {
|
||||
drawnListed = true;
|
||||
drawnWhole = false;
|
||||
drawnRects.clear();
|
||||
pending.drawn.push_back({this, guestBytes, std::chrono::steady_clock::now()});
|
||||
pending.drawnCount.store(pending.drawn.size(), std::memory_order_release);
|
||||
}
|
||||
if (!rectangle || pendingFromOthers) drawnWhole = true;
|
||||
if (!drawnWhole && right > left && bottom > top) addDrawnRect({left, top, right, bottom});
|
||||
return !drawnWhole && drawnRects.size() <= MaxDrawnRects;
|
||||
}
|
||||
|
||||
void StorageTexture::addDrawnRect(DrawnRect rect) {
|
||||
const auto inside = [](const DrawnRect& outer, const DrawnRect& inner) { return outer.left <= inner.left && outer.top <= inner.top && inner.right <= outer.right && inner.bottom <= outer.bottom; };
|
||||
if (std::any_of(drawnRects.begin(), drawnRects.end(), [&](const DrawnRect& kept) { return inside(kept, rect); })) return;
|
||||
for (auto kept = drawnRects.begin(); kept != drawnRects.end();) {
|
||||
const bool columns = kept->left == rect.left && kept->right == rect.right && kept->top <= rect.bottom && rect.top <= kept->bottom;
|
||||
const bool rows = kept->top == rect.top && kept->bottom == rect.bottom && kept->left <= rect.right && rect.left <= kept->right;
|
||||
if (!columns && !rows && !inside(rect, *kept)) {
|
||||
++kept;
|
||||
continue;
|
||||
}
|
||||
rect = {std::min(kept->left, rect.left), std::min(kept->top, rect.top), std::max(kept->right, rect.right), std::max(kept->bottom, rect.bottom)};
|
||||
drawnRects.erase(kept);
|
||||
kept = drawnRects.begin();
|
||||
}
|
||||
drawnRects.push_back(rect);
|
||||
}
|
||||
|
||||
void StorageTexture::forgetDrawn() {
|
||||
pendingFromOthers = false;
|
||||
if (!drawnListed) return;
|
||||
drawnListed = false;
|
||||
drawnWhole = false;
|
||||
drawnRects.clear();
|
||||
auto& pending = Pending();
|
||||
std::erase_if(pending.drawn, [this](const PendingWrites::Drawn& entry) { return entry.texture == this; });
|
||||
pending.drawnCount.store(pending.drawn.size(), std::memory_order_release);
|
||||
}
|
||||
|
||||
void StorageTexture::noteStore(std::uint64_t note) {
|
||||
if (note == 0) return;
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
storeNotes.push_back(note);
|
||||
if (storeNotes.size() > 8) storeNotes.erase(storeNotes.begin());
|
||||
}
|
||||
|
||||
StorageTexture::DrawnArea StorageTexture::drawnArea() const {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
if (!drawnListed) return {};
|
||||
DrawnArea area{true, !drawnWhole && !pendingFromOthers};
|
||||
if (!area.rect || drawnRects.empty()) return area;
|
||||
area.left = area.top = std::numeric_limits<std::uint32_t>::max();
|
||||
for (const auto& rect : drawnRects) {
|
||||
area.left = std::min(area.left, rect.left);
|
||||
area.top = std::min(area.top, rect.top);
|
||||
area.right = std::max(area.right, rect.right);
|
||||
area.bottom = std::max(area.bottom, rect.bottom);
|
||||
}
|
||||
area.rects = drawnRects;
|
||||
return area;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> StorageTexture::drawnSpans(const DrawnArea& area, std::span<const std::pair<std::uint64_t, std::uint64_t>> keep) const {
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> spans;
|
||||
if (area.rects.empty()) return spans;
|
||||
const auto& mip = mips.front();
|
||||
const auto elementBytes = static_cast<std::uint64_t>(BytesPerElement(descriptor.format));
|
||||
const auto rowBase = descriptor.baseAddress + mip.tiledOffset;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> columns;
|
||||
std::size_t next = 0;
|
||||
for (auto row = area.top; row < area.bottom; ++row) {
|
||||
columns.clear();
|
||||
for (const auto& rect : area.rects) {
|
||||
if (rect.top <= row && row < rect.bottom) columns.emplace_back(rect.left, rect.right);
|
||||
}
|
||||
std::sort(columns.begin(), columns.end());
|
||||
const auto rowStart = rowBase + std::uint64_t{row} * mip.pitchBytes;
|
||||
for (std::size_t first = 0; first < columns.size();) {
|
||||
auto right = columns[first].second;
|
||||
auto last = first + 1;
|
||||
for (; last < columns.size() && columns[last].first <= right; ++last) right = std::max(right, columns[last].second);
|
||||
const auto rowBegin = rowStart + columns[first].first * elementBytes;
|
||||
const auto rowEnd = rowStart + right * elementBytes;
|
||||
first = last;
|
||||
while (next < keep.size() && keep[next].second <= rowBegin) ++next;
|
||||
for (auto index = next; index < keep.size() && keep[index].first < rowEnd; ++index) {
|
||||
const auto from = std::max(rowBegin, keep[index].first);
|
||||
const auto to = std::min(rowEnd, keep[index].second);
|
||||
if (from < to) spans.emplace_back(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::storeDrawnRect(const DrawnArea& area, std::span<const ImportSegment> segments, std::span<const std::pair<std::uint64_t, std::uint64_t>> stores, const std::vector<std::uint32_t>& saveUnits) {
|
||||
const auto& mip = mips.front();
|
||||
const auto elementBytes = static_cast<std::uint64_t>(BytesPerElement(descriptor.format));
|
||||
const auto rowBase = descriptor.baseAddress + mip.tiledOffset;
|
||||
const auto width = area.right - area.left;
|
||||
const auto height = area.bottom - area.top;
|
||||
const auto rowBytes = std::uint64_t{width} * elementBytes;
|
||||
const auto hullBegin = stores.front().first;
|
||||
const auto hullBytes = static_cast<std::size_t>(stores.back().second - hullBegin);
|
||||
std::uint64_t storedBytes = 0;
|
||||
for (const auto& [from, to] : stores) storedBytes += to - from;
|
||||
auto scratch = std::make_shared<DeviceBuffer>(context, static_cast<std::size_t>(rowBytes * height), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
auto* recorder = Recorder::Active();
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
auto timing = Recorder::NoTiming;
|
||||
if (recorder != nullptr) {
|
||||
recorder->FlushKeyStoresOverlapping(hullBegin, hullBytes);
|
||||
recorder->FlushStoresOverlapping(hullBegin, hullBytes);
|
||||
commands = recorder->Commands();
|
||||
timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageWriteBack);
|
||||
recorder->Keep(scratch, scratch->Size());
|
||||
if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self));
|
||||
Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, 3);
|
||||
if (Recorder::BarrierValidate()) {
|
||||
const std::pair<VkImage, bool> read{image, false};
|
||||
recorder->NoteAccess(Recorder::CommandClass::StorageWriteBack, Recorder::Access{{}, stores, std::span(&read, 1), VK_PIPELINE_STAGE_TRANSFER_BIT});
|
||||
}
|
||||
} else {
|
||||
batch = std::make_unique<CommandBatch>(context);
|
||||
commands = batch->Handle();
|
||||
}
|
||||
VkImageMemoryBarrier toSource{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
toSource.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
||||
toSource.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
toSource.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
toSource.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
toSource.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toSource.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
toSource.image = image;
|
||||
toSource.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, descriptor.mipCount, 0, geometry.imageLayers};
|
||||
const VkMemoryBarrier importReady{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toSource);
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageOffset = {static_cast<std::int32_t>(area.left), static_cast<std::int32_t>(area.top), 0};
|
||||
region.imageExtent = {width, height, 1u};
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, scratch->Handle(), 1, ®ion);
|
||||
const auto scratchRead = WholeBufferBarrier(scratch->Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &scratchRead, 0, nullptr);
|
||||
const auto scratchOffset = [&](std::uint64_t address) {
|
||||
const auto offset = address - rowBase;
|
||||
const auto row = offset / mip.pitchBytes;
|
||||
return (row - area.top) * rowBytes + offset - row * mip.pitchBytes - area.left * elementBytes;
|
||||
};
|
||||
const auto copyBuffer = context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
|
||||
for (const auto& segment : segments) {
|
||||
std::vector<VkBufferCopy> copies;
|
||||
for (const auto& [from, to] : stores) {
|
||||
const auto begin = std::max(from, segment.begin);
|
||||
const auto end = std::min(to, segment.end);
|
||||
if (begin < end) copies.push_back({scratchOffset(begin), HostImportOffset(*segment.import, begin), end - begin});
|
||||
}
|
||||
if (!copies.empty()) copyBuffer(commands, scratch->Handle(), segment.import->buffer, static_cast<std::uint32_t>(copies.size()), copies.data());
|
||||
}
|
||||
if (!saveUnits.empty()) {
|
||||
const auto saved = savedBytes->Bytes();
|
||||
for (const auto unit : saveUnits) {
|
||||
const auto offset = static_cast<std::size_t>(layerBegin(unit) - descriptor.baseAddress);
|
||||
std::memcpy(saved.data() + offset, original.data() + offset, static_cast<std::size_t>(layerBytes(unit)));
|
||||
}
|
||||
std::vector<VkBufferCopy> saves;
|
||||
for (const auto& [from, to] : stores) saves.push_back({scratchOffset(from), from - descriptor.baseAddress, to - from});
|
||||
copyBuffer(commands, scratch->Handle(), savedBytes->Handle(), static_cast<std::uint32_t>(saves.size()), saves.data());
|
||||
}
|
||||
VkImageMemoryBarrier backToGeneral = toSource;
|
||||
backToGeneral.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
backToGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
||||
backToGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
backToGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
const VkMemoryBarrier stored{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_READ_BIT};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &stored, 0, nullptr, 1, &backToGeneral);
|
||||
if (batch) {
|
||||
batch->SubmitAndWait();
|
||||
return storedBytes;
|
||||
}
|
||||
recorder->EndGpuTiming(timing, storedBytes);
|
||||
recorder->MarkShaderReadsCovered();
|
||||
recorder->NotePendingWrite(hullBegin, hullBytes);
|
||||
noteStore(recorder->LastWriteNote(hullBegin, hullBytes));
|
||||
return storedBytes;
|
||||
}
|
||||
|
||||
void StorageTexture::Flush() {
|
||||
{
|
||||
auto& live = Live();
|
||||
@@ -3930,9 +4192,22 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
|
||||
queuedReadHeld.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
std::unique_lock<GuestMemory::GpuMutexType> drawnHold;
|
||||
if (auto& drawnPending = Pending(); drawnPending.drawnCount.load(std::memory_order_acquire) != 0 && !GuestMemory::GpuMutex().HeldByThisThread()) {
|
||||
bool overlap = false;
|
||||
{
|
||||
std::lock_guard lock(drawnPending.mutex);
|
||||
overlap = std::any_of(drawnPending.drawn.begin(), drawnPending.drawn.end(), [&](const PendingWrites::Drawn& entry) { return entry.texture->dirty && entry.texture->overlaps(address, bytes); });
|
||||
}
|
||||
if (overlap) {
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
drawnHold = std::unique_lock(GuestMemory::GpuMutex());
|
||||
}
|
||||
}
|
||||
// The images stay alive across the scan: their last owner may be a batch's kept list, which
|
||||
// another thread releases outside the GPU mutex once the batch completed.
|
||||
std::vector<std::shared_ptr<StorageTexture>> flush;
|
||||
std::vector<std::uint64_t> ownNotes;
|
||||
{
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
@@ -3960,8 +4235,12 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
|
||||
for (const auto& texture : flush) pending.flushing.push_back(texture.get());
|
||||
pending.flushingCount.store(pending.flushing.size(), std::memory_order_release);
|
||||
if (!flush.empty()) BumpPendingSerial();
|
||||
for (const auto& texture : flush) {
|
||||
if (texture->drawnListed) ownNotes.insert(ownNotes.end(), texture->storeNotes.begin(), texture->storeNotes.end());
|
||||
}
|
||||
}
|
||||
if (flush.empty()) {
|
||||
if (drawnHold.owns_lock()) drawnHold.unlock();
|
||||
const bool stored = WaitForFlushing(address, bytes, except);
|
||||
// Units shadowed over the range (another image's results retiled into the import's unit
|
||||
// shadow, or an earlier flush's) still reach the import as the scope asks.
|
||||
@@ -3985,10 +4264,11 @@ bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, cons
|
||||
for (const auto& texture : flush) std::fprintf(stderr, "[flush] image 0x%llx+0x%llx for %s 0x%llx+0x%zx\n", static_cast<unsigned long long>(texture->descriptor.baseAddress), static_cast<unsigned long long>(texture->guestBytes), reason, static_cast<unsigned long long>(address), bytes);
|
||||
}
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes) != 0) {
|
||||
if (!ownNotes.empty() && (flush.size() != 1 || std::any_of(flush.begin(), flush.end(), [](const std::shared_ptr<StorageTexture>& texture) { return ExistingHostImportFor(texture->context, texture->descriptor.baseAddress, static_cast<std::size_t>(texture->guestBytes)) == nullptr; }))) ownNotes.clear();
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && !Recorder::InCompletion() && recorder->PendingWriterSerial(address, bytes, ownNotes) != 0) {
|
||||
try {
|
||||
recorder->Reap();
|
||||
if (const auto writer = recorder->PendingWriterSerial(address, bytes); writer == recorder->OpenSerial()) {
|
||||
if (const auto writer = recorder->PendingWriterSerial(address, bytes, ownNotes); writer == recorder->OpenSerial()) {
|
||||
Recorder::CountSync(recorder->OpenWriteOverlaps(address, bytes) ? 1 : 3);
|
||||
recorder->Sync();
|
||||
} else if (writer != 0) {
|
||||
@@ -4098,7 +4378,8 @@ void StorageTexture::saveBeforeUnwatch() {
|
||||
const auto end = begin + layerBytes(unit);
|
||||
const auto spanBegin = begin & ~(block - 1);
|
||||
const auto spanBlocks = static_cast<std::size_t>((end - spanBegin + block - 1) / block);
|
||||
const std::vector<std::uint64_t> generations(spanBlocks, layerGeneration[unit]);
|
||||
std::vector<std::uint64_t> generations(spanBlocks);
|
||||
for (std::size_t k = 0; k < spanBlocks; ++k) generations[k] = unitBlockGeneration(unit, spanBegin + k * block);
|
||||
std::vector<std::uint8_t> changedBlocks(spanBlocks);
|
||||
if (!GuestMemory::ChangedBlocks(begin, static_cast<std::size_t>(end - begin), generations, changedBlocks)) continue;
|
||||
forgetKeptAtUnwatch(unit);
|
||||
@@ -4106,7 +4387,7 @@ void StorageTexture::saveBeforeUnwatch() {
|
||||
const auto from = std::max(at, begin);
|
||||
const auto to = std::min(at + block, end);
|
||||
const bool edge = from != at || to != at + block;
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[unit])) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]);
|
||||
if (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (!edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), unitBlockGeneration(unit, at))) && !unchangedSinceBaseline(from, to)) keptAtUnwatch.emplace(from, layerGeneration[unit]);
|
||||
}
|
||||
layerOriginal[unit] = true;
|
||||
cancelStoredSave(unit);
|
||||
@@ -4270,6 +4551,49 @@ void StorageTexture::StoreReadbackBeforeLabel(const char* reason) {
|
||||
for (const auto& [address, bytes] : ranges) static_cast<void>(FlushPending(address, bytes, nullptr, reason));
|
||||
}
|
||||
|
||||
std::size_t StorageTexture::StoreDrawnLinear(const char* reason) {
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
|
||||
{
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& entry : pending.drawn) {
|
||||
if (entry.texture->dirty) ranges.emplace_back(entry.texture->descriptor.baseAddress, static_cast<std::size_t>(entry.texture->guestBytes));
|
||||
}
|
||||
}
|
||||
std::size_t stored = 0;
|
||||
for (const auto& [address, bytes] : ranges) {
|
||||
if (FlushPending(address, bytes, nullptr, reason)) ++stored;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
std::size_t StorageTexture::StoreDrawnLinearTouching(const char* reason, const std::function<bool(std::uint64_t, std::uint64_t)>& touches, bool all) {
|
||||
auto& pending = Pending();
|
||||
if (pending.drawnCount.load(std::memory_order_acquire) == 0 || Recorder::InCompletion()) return 0;
|
||||
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
|
||||
{
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& entry : pending.drawn) {
|
||||
if (!entry.texture->dirty) continue;
|
||||
const auto begin = entry.texture->descriptor.baseAddress;
|
||||
const auto end = begin + entry.texture->guestBytes;
|
||||
if (!all && !touches(begin, end)) continue;
|
||||
ranges.emplace_back(begin, static_cast<std::size_t>(entry.texture->guestBytes));
|
||||
}
|
||||
}
|
||||
if (ranges.empty()) return 0;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
std::size_t stored = 0;
|
||||
for (const auto& [address, bytes] : ranges) {
|
||||
if (FlushPending(address, bytes, nullptr, reason)) ++stored;
|
||||
}
|
||||
return stored;
|
||||
}
|
||||
|
||||
bool StorageTexture::AnyPending() {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
@@ -4336,37 +4660,96 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function<std::uint64_t()>& stamp) {
|
||||
std::vector<std::pair<std::shared_ptr<StorageTexture>, std::uint32_t>> kept;
|
||||
struct Candidate {
|
||||
std::shared_ptr<StorageTexture> texture;
|
||||
std::uint32_t unit;
|
||||
bool drawn;
|
||||
};
|
||||
struct Kept {
|
||||
std::shared_ptr<StorageTexture> texture;
|
||||
std::uint32_t unit;
|
||||
std::uint64_t from;
|
||||
std::uint64_t to;
|
||||
std::uint64_t since;
|
||||
bool whole;
|
||||
bool drawn;
|
||||
};
|
||||
std::vector<Candidate> candidates;
|
||||
if (recorded != 0 && bytes != 0) {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (auto* texture : pending.textures) {
|
||||
if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue;
|
||||
const auto consider = [&](StorageTexture* texture) {
|
||||
if ((!texture->blockUnits && !texture->drawnListed) || !texture->overlaps(address, bytes)) return;
|
||||
auto alive = texture->weak_from_this().lock();
|
||||
if (alive == nullptr) continue;
|
||||
if (alive == nullptr) return;
|
||||
for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) {
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto unitBytes = texture->layerBytes(unit);
|
||||
if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue;
|
||||
if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit);
|
||||
if (texture->layerGeneration[unit] >= recorded) candidates.push_back({alive, unit, texture->drawnListed});
|
||||
}
|
||||
};
|
||||
for (auto* texture : pending.textures) consider(texture);
|
||||
for (auto* texture : pending.flushing) consider(texture);
|
||||
}
|
||||
constexpr std::uint64_t block = 65536;
|
||||
std::vector<Kept> kept;
|
||||
for (const auto& [texture, unit, drawn] : candidates) {
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto end = begin + texture->layerBytes(unit);
|
||||
GuestMemory::CollectWritesUncached(begin, static_cast<std::size_t>(end - begin));
|
||||
if (GuestMemory::UnchangedSince(begin, static_cast<std::size_t>(end - begin), texture->layerGeneration[unit])) {
|
||||
kept.push_back({texture, unit, begin, end, texture->layerGeneration[unit], true, drawn});
|
||||
continue;
|
||||
}
|
||||
if (!drawn) continue;
|
||||
const auto first = std::max(begin, address) / block;
|
||||
const auto last = (std::min(end, address + bytes) - 1) / block;
|
||||
const auto from = std::max(begin, first * block);
|
||||
const auto to = std::min(end, (last + 1) * block);
|
||||
std::vector<std::uint64_t> generations(static_cast<std::size_t>(last - first + 1));
|
||||
for (std::size_t k = 0; k < generations.size(); ++k) generations[k] = texture->unitBlockGeneration(unit, (first + k) * block);
|
||||
std::vector<std::uint8_t> changed(generations.size());
|
||||
if (!GuestMemory::ChangedBlocks(from, static_cast<std::size_t>(to - from), generations, changed)) continue;
|
||||
for (std::size_t k = 0; k < generations.size(); ++k) {
|
||||
const auto at = (first + k) * block;
|
||||
const auto blockFrom = std::max(at, begin);
|
||||
const auto blockTo = std::min(at + block, end);
|
||||
const bool edge = blockFrom != at || blockTo != at + block;
|
||||
if (changed[k] == GuestMemory::BlockWritten && (!edge || !GuestMemory::UnchangedSince(blockFrom, static_cast<std::size_t>(blockTo - blockFrom), generations[k]))) continue;
|
||||
if (!kept.empty() && !kept.back().whole && kept.back().texture == texture && kept.back().unit == unit && kept.back().since == generations[k] && kept.back().to == blockFrom) kept.back().to = blockTo;
|
||||
else kept.push_back({texture, unit, blockFrom, blockTo, generations[k], false, true});
|
||||
}
|
||||
}
|
||||
std::erase_if(kept, [](const auto& entry) {
|
||||
const auto& [texture, unit] = entry;
|
||||
const auto begin = texture->layerBegin(unit);
|
||||
const auto unitBytes = static_cast<std::size_t>(texture->layerBytes(unit));
|
||||
GuestMemory::CollectWritesUncached(begin, unitBytes);
|
||||
return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]);
|
||||
});
|
||||
const auto generation = stamp();
|
||||
if (generation == 0 || kept.empty()) return generation;
|
||||
std::vector<Kept> confirmed;
|
||||
for (const auto& entry : kept) {
|
||||
if ((entry.whole && !entry.drawn) || GuestMemory::UnchangedSinceCollected(entry.from, static_cast<std::size_t>(entry.to - entry.from), entry.since)) {
|
||||
confirmed.push_back(entry);
|
||||
continue;
|
||||
}
|
||||
if (entry.from / block == (entry.to - 1) / block) continue;
|
||||
const auto last = (std::min(entry.to, address + bytes) - 1) / block;
|
||||
for (auto at = std::max(entry.from, address) / block * block; at <= last * block; at += block) {
|
||||
const auto from = std::max(at, entry.from);
|
||||
const auto to = std::min(at + block, entry.to);
|
||||
if (GuestMemory::UnchangedSinceCollected(from, static_cast<std::size_t>(to - from), entry.since)) confirmed.push_back({entry.texture, entry.unit, from, to, entry.since, false, true});
|
||||
}
|
||||
}
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
for (const auto& [texture, unit] : kept) {
|
||||
if (!texture->layerPending[unit]) continue;
|
||||
texture->layerGeneration[unit] = generation;
|
||||
texture->refreshGeneration();
|
||||
for (const auto& entry : confirmed) {
|
||||
auto& texture = *entry.texture;
|
||||
if (!texture.layerPending[entry.unit]) continue;
|
||||
if (entry.whole) {
|
||||
texture.layerGeneration[entry.unit] = generation;
|
||||
texture.refreshGeneration();
|
||||
continue;
|
||||
}
|
||||
const auto spanBegin = texture.descriptor.baseAddress / block;
|
||||
if (texture.completionKept.empty()) texture.completionKept.assign(static_cast<std::size_t>((texture.descriptor.baseAddress + texture.guestBytes - 1) / block - spanBegin + 1), 0);
|
||||
for (auto at = entry.from / block; at <= (entry.to - 1) / block; ++at) texture.completionKept[static_cast<std::size_t>(at - spanBegin)] = generation;
|
||||
}
|
||||
return generation;
|
||||
}
|
||||
@@ -4482,6 +4865,7 @@ bool StorageTexture::AccessKeptByCpu(std::uint64_t address, std::size_t bytes, s
|
||||
// store there would see, and the stamps only grow.
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Hook);
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
if (std::any_of(overlapping.begin(), overlapping.end(), [](const std::shared_ptr<StorageTexture>& texture) { return texture->drawnArea().drawn; })) return false;
|
||||
if (!blocksKept(overlapping, address, bytes)) return false;
|
||||
const auto site = static_cast<std::uint8_t>(GuestMemory::CurrentReadSite());
|
||||
std::size_t dropped = 0;
|
||||
@@ -4613,6 +4997,7 @@ std::size_t StorageTexture::DiscardPendingInside(std::uint64_t address, std::siz
|
||||
texture->layerPending.assign(texture->trackedLayers, false);
|
||||
texture->layerOriginal.assign(texture->trackedLayers, false);
|
||||
texture->cancelStoredSaves();
|
||||
texture->forgetDrawn();
|
||||
it = pending.textures.erase(it);
|
||||
++discarded;
|
||||
} else if (texture != exempt && texture->blockUnits && texture->overlaps(address, bytes)) {
|
||||
@@ -5222,6 +5607,12 @@ void StorageTexture::blockGenerations(std::vector<std::uint64_t>& generations) c
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t StorageTexture::unitBlockGeneration(std::uint32_t unit, std::uint64_t at) const {
|
||||
const auto unitGeneration = layerGeneration[unit];
|
||||
if (unitGeneration == 0 || !layerPending[unit] || completionKept.empty()) return unitGeneration;
|
||||
return std::max(unitGeneration, completionKept[static_cast<std::size_t>(at / 65536 - descriptor.baseAddress / 65536)]);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::atomic<bool>& ClearHandOverOn() {
|
||||
@@ -5518,10 +5909,12 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
if (!layers[layer]) continue;
|
||||
const auto end = layerBegin(layer) + layerBytes(layer);
|
||||
for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast<std::size_t>((at - spanBegin) / block)] = layerGeneration[layer];
|
||||
for (auto at = layerBegin(layer) & ~(block - 1); at < end; at += block) generations[static_cast<std::size_t>((at - spanBegin) / block)] = unitBlockGeneration(layer, at);
|
||||
}
|
||||
const auto drawn = drawnArea();
|
||||
std::vector<std::uint8_t> changedBlocks(spanBlocks);
|
||||
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks);
|
||||
std::vector<std::uint8_t> cpuBlocks(drawn.drawn ? spanBlocks : 0);
|
||||
const bool tracked = GuestMemory::ChangedBlocks(firstStored, static_cast<std::size_t>(lastStored - firstStored), generations, changedBlocks, cpuBlocks);
|
||||
std::vector<bool> compared(trackedLayers, false);
|
||||
if (!tracked) {
|
||||
awaitStoredSave(&layers);
|
||||
@@ -5546,7 +5939,8 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
if (!layers[layer]) continue;
|
||||
const auto begin = layerBegin(layer);
|
||||
const auto end = begin + layerBytes(layer);
|
||||
if (tracked ? layerGeneration[layer] == 0 : !compared[layer]) {
|
||||
const bool unknown = tracked ? layerGeneration[layer] == 0 : !compared[layer];
|
||||
if (unknown && !drawn.rect) {
|
||||
unsavedOutsideWatch = unsavedOutsideWatch || !tracked;
|
||||
keep.emplace_back(begin, end);
|
||||
continue;
|
||||
@@ -5556,12 +5950,16 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
const auto to = std::min(at + block, end);
|
||||
if (from >= to) continue;
|
||||
const bool edge = from != at || to != at + block;
|
||||
if (keptAtUnwatchHolds(from, layer) || (changedBlocks[static_cast<std::size_t>((at - spanBegin) / block)] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), layerGeneration[layer])) && !unchangedSinceBaseline(from, to))) {
|
||||
const auto index = static_cast<std::size_t>((at - spanBegin) / block);
|
||||
const bool kept = keptAtUnwatchHolds(from, layer);
|
||||
const bool written = kept || unknown || (changedBlocks[index] == GuestMemory::BlockWritten && (compared[layer] || !edge || GuestMemory::StoredOver(from, static_cast<std::size_t>(to - from), unitBlockGeneration(layer, at))) && !unchangedSinceBaseline(from, to));
|
||||
if (written && (!drawn.rect || (tracked && !kept && !unknown && cpuBlocks[index] == 0))) {
|
||||
skippedAny = true;
|
||||
skippedLayer[layer] = true;
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
if (written && drawn.rect) skippedLayer[layer] = true;
|
||||
if (!keep.empty() && keep.back().second == from) keep.back().second = to;
|
||||
else keep.emplace_back(from, to);
|
||||
}
|
||||
@@ -5575,10 +5973,15 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
const auto first = keep.empty() ? descriptor.baseAddress + guestBytes : keep.front().first;
|
||||
std::fprintf(stderr, "[flush] image 0x%llx+0x%llx keeps %zu CPU-written 64 KiB blocks (first stored byte at +0x%llx, %zu ranges stored, generation %llu)\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), skipped, static_cast<unsigned long long>(first - descriptor.baseAddress), keep.size(), static_cast<unsigned long long>(generation));
|
||||
}
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> spans;
|
||||
if (drawn.rect) spans = drawnSpans(drawn, keep);
|
||||
const auto& stores = drawn.rect ? spans : keep;
|
||||
const auto stampBegin = drawn.rect && !stores.empty() ? stores.front().first : firstStored;
|
||||
const auto stampEnd = drawn.rect && !stores.empty() ? stores.back().second : lastStored;
|
||||
// Taken before this write-back stamps anything (see advanceAdjacent), with the 64 KiB block span
|
||||
// its stamps cover.
|
||||
const auto firstBlock = firstStored / block;
|
||||
const auto lastBlock = (lastStored - 1) / block;
|
||||
const auto firstBlock = stampBegin / block;
|
||||
const auto lastBlock = (stampEnd - 1) / block;
|
||||
const auto adjacent = adjacentPendingUnchanged(firstBlock, lastBlock);
|
||||
// After the store: the stored layers are in sync at a fresh generation unless blocks were kept
|
||||
// for the CPU (then the layer is stale there, keeps its old generation and re-uploads at its
|
||||
@@ -5615,7 +6018,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
advanceAdjacent(adjacent, now, firstBlock, lastBlock);
|
||||
if (!shadowed.empty()) markShadowed(now);
|
||||
};
|
||||
if (keep.empty()) {
|
||||
if (stores.empty()) {
|
||||
// Every selected block was written by the CPU since the layer's generation: nothing to
|
||||
// store, the keys stay as they are, and the image is stale there (re-uploaded at its next
|
||||
// use, never from a matching `original`).
|
||||
@@ -5645,6 +6048,24 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
++Profile().storageDirectWriteBacks;
|
||||
return;
|
||||
}
|
||||
if (drawn.rect) {
|
||||
PublishShadow(stampBegin, static_cast<std::size_t>(stampEnd - stampBegin), PublishScope::PartialUnits, PublishReason::Upload);
|
||||
const auto storedBytes = storeDrawnRect(drawn, segments, stores, saveUnits);
|
||||
countStorageWriteBack(storedBytes, true, true);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
forgetStored();
|
||||
uploadedKeys = DccKeys::Uncompressed;
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> stamps;
|
||||
for (const auto& [from, to] : stores) {
|
||||
if (!stamps.empty() && from / block <= (stamps.back().second - 1) / block + 1) stamps.back().second = to;
|
||||
else stamps.emplace_back(from, to);
|
||||
}
|
||||
for (const auto& [from, to] : stamps) GuestMemory::MarkWritten(from, static_cast<std::size_t>(to - from));
|
||||
settle(true);
|
||||
commitStoredSave(saveUnits);
|
||||
++Profile().storageDirectWriteBacks;
|
||||
return;
|
||||
}
|
||||
// The whole-layer copies below overwrite the interior units of the span; the edge units
|
||||
// keep a neighbour's shadowed results in the rest of their bytes, which reach the import
|
||||
// first (this write-back's stamps then make those units stale).
|
||||
@@ -5747,8 +6168,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
recorder->EndGpuTiming(timing, storedBytes);
|
||||
recorder->MarkShaderReadsCovered();
|
||||
recorder->NotePendingWrite(firstStored, static_cast<std::size_t>(lastStored - firstStored));
|
||||
if (drawn.drawn) noteStore(recorder->LastWriteNote(firstStored, static_cast<std::size_t>(lastStored - firstStored)));
|
||||
}
|
||||
countStorageWriteBack(storedBytes, true);
|
||||
countStorageWriteBack(storedBytes, true, drawn.drawn);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
// The guest bytes now differ from `original`; other caches of the range see the write.
|
||||
forgetStored();
|
||||
@@ -5789,10 +6211,10 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
overhang = covered >= end;
|
||||
}
|
||||
}
|
||||
const bool registered = std::all_of(keep.begin(), keep.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast<std::size_t>(range.second - range.first)); });
|
||||
const bool registered = std::all_of(stores.begin(), stores.end(), [](const auto& range) { return RegisteredReadableCovers(range.first, static_cast<std::size_t>(range.second - range.first)); });
|
||||
if (unregisteredDrop && !overhang && !registered) {
|
||||
static std::atomic<int> reports{0};
|
||||
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), keep.size());
|
||||
if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast<unsigned long long>(descriptor.baseAddress), static_cast<unsigned long long>(guestBytes), stores.size());
|
||||
unregisteredDropped.fetch_add(1, std::memory_order_relaxed);
|
||||
forgetStored();
|
||||
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
|
||||
@@ -5809,6 +6231,10 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
std::vector<bool> unsaved = layerOriginal;
|
||||
unsaved.flip();
|
||||
for (const auto& [begin, end] : unitRuns(unsaved)) {
|
||||
if (drawn.rect) {
|
||||
GuestMemory::ReadCommitted(descriptor.baseAddress + begin, std::span(original).subspan(static_cast<std::size_t>(begin), static_cast<std::size_t>(end - begin)));
|
||||
continue;
|
||||
}
|
||||
for (const auto& [from, to] : keep) {
|
||||
const auto first = std::max(descriptor.baseAddress + begin, from);
|
||||
const auto last = std::min(descriptor.baseAddress + end, to);
|
||||
@@ -5870,7 +6296,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback submit");
|
||||
batch.SubmitAndWait();
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback done");
|
||||
countStorageWriteBack(guestBytes, false);
|
||||
std::uint64_t storedBytes = 0;
|
||||
for (const auto& [from, to] : stores) storedBytes += to - from;
|
||||
countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn);
|
||||
if (profile) Profile().storageGpu += timer.lap();
|
||||
if (dump) {
|
||||
char name[64];
|
||||
@@ -5885,7 +6313,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
// Store only the kept ranges' blocks that changed, so concurrent CPU writes to untouched texels
|
||||
// survive; `original` follows for the stored layers (the others' guest bytes are unchanged).
|
||||
const auto current = host.Bytes();
|
||||
for (const auto& [from, to] : keep) {
|
||||
for (const auto& [from, to] : stores) {
|
||||
const auto offset = static_cast<std::size_t>(from - descriptor.baseAddress);
|
||||
const auto length = static_cast<std::size_t>(to - from);
|
||||
GuestMemory::WriteChangedCommitted(from, current.subspan(offset, length), std::span<const std::byte>(original).subspan(offset, length));
|
||||
@@ -5921,6 +6349,7 @@ StorageTexture::~StorageTexture() {
|
||||
{
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
forgetDrawn();
|
||||
if (dirty) {
|
||||
dirty = false;
|
||||
pending.textures.remove(this);
|
||||
|
||||
@@ -141,6 +141,7 @@ struct Step {
|
||||
const std::uint32_t* pixel;
|
||||
std::uint64_t pointer;
|
||||
std::array<std::uint32_t, 4> descriptor{};
|
||||
std::uint64_t color = 0;
|
||||
};
|
||||
|
||||
void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list<Step> steps, bool linear = false) {
|
||||
@@ -179,6 +180,10 @@ void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializ
|
||||
Append(commands, 0x76, {0x008, static_cast<std::uint32_t>(pixel >> 8u), static_cast<std::uint32_t>(pixel >> 40u)});
|
||||
Append(commands, 0x76, {0x00e, static_cast<std::uint32_t>(step.pointer), static_cast<std::uint32_t>(step.pointer >> 32u)});
|
||||
Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]});
|
||||
if (step.color != 0) {
|
||||
Append(commands, 0x69, {0x318, static_cast<std::uint32_t>(step.color >> 8u)});
|
||||
Append(commands, 0x69, {0x390, static_cast<std::uint32_t>(step.color >> 40u)});
|
||||
}
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
@@ -285,6 +290,26 @@ void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target,
|
||||
if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after));
|
||||
}
|
||||
|
||||
void TargetReadSynchronously(const GuestBlock& vertices, const GuestBlock& target, const GuestBlock& unmapped) {
|
||||
Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, true);
|
||||
std::memset(target.Bytes(ColorOffset), 0, ColorBytes);
|
||||
std::memset(unmapped.Bytes(ColorOffset), 0, ColorBytes);
|
||||
const auto before = AddressDraws();
|
||||
const auto otherBefore = OtherDraws();
|
||||
Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset), {}, unmapped.At(ColorOffset)}}, true);
|
||||
const auto after = AddressDraws();
|
||||
const auto otherAfter = OtherDraws();
|
||||
const unsigned red = unmapped.Bytes(ColorOffset)[0];
|
||||
std::printf("a synchronous draw reading through the table the linear target the previous draw rendered read %s (red byte %u); recorded: %s, %s\n", red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str());
|
||||
if (otherAfter.draws == otherBefore.draws) {
|
||||
std::puts("a synchronous draw reading through the table the linear target the previous draw rendered: skipped, that draw was not recorded because the linear target is not resident on this device, so the target was stored right after it");
|
||||
return;
|
||||
}
|
||||
Expect(otherAfter.draws - otherBefore.draws == 1, "the draw rendering the linear target was not recorded as one draw: " + Counts(otherBefore, otherAfter, "other draws"));
|
||||
Expect(after.draws == before.draws, "the draw reading through the table into an unregistered target was recorded, so the case does not test a synchronous draw: " + Counts(before, after));
|
||||
Expect(red == 255u, "a synchronous draw reading through the table the linear target the previous draw rendered read the bytes before it (red byte " + std::to_string(red) + "), not that draw's color");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -306,6 +331,7 @@ int main(int argc, char** argv) {
|
||||
GuestBlock readOnlyVertices;
|
||||
GuestBlock data;
|
||||
GuestBlock target;
|
||||
GuestBlock unmapped;
|
||||
FillVertices(readOnlyVertices);
|
||||
readOnlyVertices.Map(false);
|
||||
data.Map(true);
|
||||
@@ -320,6 +346,7 @@ int main(int argc, char** argv) {
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, false);
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, true);
|
||||
}
|
||||
if (only.empty() || only == "--synchronous-target-read") TargetReadSynchronously(readOnlyVertices, target, unmapped);
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
#include "prx/libkernel/include/Api.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libkernel/Equeue/Equeue.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::size_t Pitch = std::size_t{Width} * 4u;
|
||||
constexpr std::size_t TargetBytes = Pitch * Height;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t LabelOffset = 3 * BlockBytes;
|
||||
constexpr std::uint32_t Untouched = 0x5a5a5a5au;
|
||||
constexpr std::uint32_t Drawn = 0xff0000ffu;
|
||||
constexpr std::uint32_t LabelValue = 0x600dcafeu;
|
||||
constexpr std::uint32_t OutputHandle = 9;
|
||||
constexpr std::uint32_t Rgba8UnormInfo = 0x28028u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::array<std::uint32_t, 4> Scissor{32, 16, 160, 80};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> FillCode{
|
||||
0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(4) std::atomic<std::uint32_t> Gate = 0;
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
void AppendDraw(std::vector<std::uint32_t>& commands, std::uint64_t target) {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto extent = (Height << 16u) | Width;
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0x01200000}, {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, Rgba8UnormInfo},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)},
|
||||
{0x3b8, LinearAttrib3}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, extent},
|
||||
{0x81, 0x80000000}, {0x82, extent},
|
||||
{0x90, 0x80000000u | (Scissor[1] << 16u) | Scissor[0]}, {0x91, (Scissor[3] << 16u) | Scissor[2]},
|
||||
{0x94, 0x80000000}, {0x95, extent},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(2.0f * Width)}, {0x110, std::bit_cast<std::uint32_t>(2.0f * Width)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(2.0f * Height)}, {0x112, std::bit_cast<std::uint32_t>(2.0f * Height)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
{0x318, static_cast<std::uint32_t>(target >> 8u)}, {0x390, static_cast<std::uint32_t>(target >> 40u)},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
Append(commands, 0x79, {0x24a, 0});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
|
||||
void AppendGate(std::vector<std::uint32_t>& commands) {
|
||||
const auto gate = reinterpret_cast<std::uintptr_t>(&Gate);
|
||||
commands.insert(commands.end(), {0xc0053c00u, 0x13u, static_cast<std::uint32_t>(gate), static_cast<std::uint32_t>(gate >> 32u), 1u, 0xffffffffu, 0x19u});
|
||||
}
|
||||
|
||||
void Submit(std::vector<std::uint32_t>& commands) {
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
|
||||
class Region {
|
||||
public:
|
||||
Region(std::size_t bytes, bool watched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "linear target completion points: cannot map the guest region");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "linear target completion points: cannot allocate the guest region");
|
||||
auto* words = reinterpret_cast<std::uint32_t*>(data);
|
||||
std::fill(words, words + bytes / 4u, Untouched);
|
||||
if (arena && !watched) {
|
||||
AgcDriver::GuestMemory::Unwatch(Address(), bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "linear target completion points: the region is still write-watched");
|
||||
}
|
||||
GuestAllocations::Mutation().Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Region() {
|
||||
GuestAllocations::Mutation().Remove(data);
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Region(const Region&) = delete;
|
||||
Region& operator=(const Region&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::uint32_t Word(std::size_t offset) const { return std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).load(std::memory_order_acquire); }
|
||||
std::size_t bytes;
|
||||
|
||||
private:
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
void Import(const Region& region) {
|
||||
const auto address = region.Address();
|
||||
const std::vector<std::uint32_t> userData{
|
||||
static_cast<std::uint32_t>(address),
|
||||
static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u),
|
||||
static_cast<std::uint32_t>(region.bytes / 16u),
|
||||
0x4bfacu,
|
||||
Untouched,
|
||||
Untouched,
|
||||
Untouched,
|
||||
Untouched,
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
const auto code = reinterpret_cast<std::uintptr_t>(FillCode.data());
|
||||
Append(commands, 0x76, {0x207, 64, 1, 1});
|
||||
Append(commands, 0x76, {0x20c, static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)});
|
||||
Append(commands, 0x76, {0x213, (static_cast<std::uint32_t>(userData.size()) << 1u) | 0x80u});
|
||||
commands.push_back(0xc0007600u | (static_cast<std::uint32_t>(userData.size()) << 16u));
|
||||
commands.push_back(0x240u);
|
||||
commands.insert(commands.end(), userData.begin(), userData.end());
|
||||
commands.insert(commands.end(), {0xc0031500u, static_cast<std::uint32_t>(region.bytes / 1024u), 1, 1, 0x41u});
|
||||
Submit(commands);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
bool InsideScissor(std::uint32_t x, std::uint32_t y) {
|
||||
return x >= Scissor[0] && x < Scissor[2] && y >= Scissor[1] && y < Scissor[3];
|
||||
}
|
||||
|
||||
std::string Mismatches(const Region& region) {
|
||||
std::size_t drawn = 0, outside = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto value = region.Word(y * Pitch + x * 4u);
|
||||
if (InsideScissor(x, y)) drawn += value == Drawn ? 0u : 1u;
|
||||
else outside += value == Untouched ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
if (drawn == 0 && outside == 0) return {};
|
||||
return std::to_string(drawn) + " of the " + std::to_string((Scissor[2] - Scissor[0]) * (Scissor[3] - Scissor[1])) + " pixels inside the scissor do not hold the draw's color and " + std::to_string(outside) + " pixels outside it changed";
|
||||
}
|
||||
|
||||
bool Await(const std::function<bool()>& done) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!done()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class Request final : public AgcDriver::IFlipRequest {
|
||||
public:
|
||||
explicit Request(std::atomic<int>& ready) : ready(ready) {}
|
||||
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ++ready; }
|
||||
void Fail(std::exception_ptr) noexcept override {}
|
||||
private:
|
||||
std::atomic<int>& ready;
|
||||
};
|
||||
|
||||
class Output final : public AgcDriver::IVideoOutput {
|
||||
public:
|
||||
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared<Request>(ready); }
|
||||
void Fail(std::exception_ptr) noexcept override {}
|
||||
std::atomic<int> ready = 0;
|
||||
};
|
||||
|
||||
enum class Point { Flip, Label, Interrupt };
|
||||
|
||||
struct Events {
|
||||
KernelEqueue queue = 0;
|
||||
KernelEqueueRef owner;
|
||||
int tag = 0;
|
||||
};
|
||||
|
||||
std::string Run(Point point, bool watched, const Region& region, Output& output, Events& events) {
|
||||
const char* name = point == Point::Flip ? "flip" : point == Point::Label ? "label" : "end-of-pipe interrupt";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendDraw(commands, target);
|
||||
if (point == Point::Flip) {
|
||||
commands.insert(commands.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0});
|
||||
} else if (point == Point::Label) {
|
||||
const auto label = target + LabelOffset;
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast<std::uint32_t>(label), static_cast<std::uint32_t>(label >> 32u), LabelValue, 0, 0});
|
||||
} else {
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x06000528u, 2u << 24u, 0, 0, 0, 0, 0});
|
||||
}
|
||||
AppendGate(commands);
|
||||
const auto readyBefore = output.ready.load();
|
||||
if (point == Point::Interrupt) {
|
||||
std::array<KernelEvent, 4> stale{};
|
||||
while (events.owner->GetTriggeredEvents(stale.data(), static_cast<int>(stale.size())) > 0) {}
|
||||
}
|
||||
Submit(commands);
|
||||
const bool watchedAtStore = AgcDriver::GuestMemory::Watched(target, TargetBytes);
|
||||
std::string failure;
|
||||
if (point == Point::Flip) {
|
||||
const bool flipped = Await([&] { return output.ready.load() != readyBefore; });
|
||||
if (!flipped) failure = "the flip did not reach the GPU";
|
||||
else if (!Await([&] { return Mismatches(region).empty(); })) failure = "after the flip, while the queue waited: " + Mismatches(region);
|
||||
} else if (point == Point::Label) {
|
||||
if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed";
|
||||
else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the label after the draw landed: " + mismatches;
|
||||
} else {
|
||||
std::array<KernelEvent, 1> delivered{};
|
||||
if (!Await([&] { return events.owner->GetTriggeredEvents(delivered.data(), 1) == 1; })) failure = "the end-of-pipe interrupt after the draw was not delivered";
|
||||
else if (delivered[0].udata != &events.tag) failure = "an unexpected event was delivered";
|
||||
else if (const auto mismatches = Mismatches(region); !mismatches.empty()) failure = "when the end-of-pipe interrupt after the draw was delivered: " + mismatches;
|
||||
}
|
||||
Gate.store(1, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target completion point: %s, %s memory (watched at the store: %d): %s\n", name, watched ? "write-watched" : "unwatched", watchedAtStore ? 1 : 0, failure.empty() ? "stored" : failure.c_str());
|
||||
if (failure.empty()) return {};
|
||||
return std::string(name) + (watched ? " (write-watched memory): " : " (unwatched memory): ") + failure;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
bool flips = argc == 1, labels = argc == 1, interrupts = argc == 1, watchedFlip = argc == 1;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string_view argument(argv[index]);
|
||||
if (argument == "--flip") flips = true;
|
||||
else if (argument == "--label") labels = true;
|
||||
else if (argument == "--interrupt") interrupts = true;
|
||||
else Require(false, "linear target completion points: unknown argument " + std::string(argument));
|
||||
}
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) {
|
||||
std::puts("linear target completion point tests skipped: no host import on this device, so the target is never resident");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
std::vector<std::unique_ptr<Region>> regions;
|
||||
struct ShutdownGuard {
|
||||
~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} }
|
||||
} shutdown;
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
auto output = std::make_shared<Output>();
|
||||
AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output);
|
||||
Events events;
|
||||
if (interrupts) {
|
||||
Require(sceKernelCreateEqueue(&events.queue, "AGC linear target") == 0, "linear target completion points: event queue creation failed");
|
||||
events.owner = EqueuePin_nid_postfix(events.queue);
|
||||
Require(sceAgcDriverAddEqEvent(events.queue, 0, &events.tag) == 0, "linear target completion points: graphics event registration failed");
|
||||
}
|
||||
std::vector<std::string> failures;
|
||||
const auto run = [&](Point point, bool watched) {
|
||||
regions.push_back(std::make_unique<Region>(watched ? 16 * BlockBytes : 4 * BlockBytes, watched));
|
||||
if (auto failure = Run(point, watched, *regions.back(), *output, events); !failure.empty()) failures.push_back(std::move(failure));
|
||||
};
|
||||
if (flips) run(Point::Flip, false);
|
||||
if (watchedFlip) run(Point::Flip, true);
|
||||
if (labels) run(Point::Label, false);
|
||||
if (interrupts) run(Point::Interrupt, false);
|
||||
if (interrupts) {
|
||||
Require(sceAgcDriverDeleteEqEvent(events.queue, 0) == 0, "linear target completion points: graphics event deletion failed");
|
||||
events.owner.reset();
|
||||
Require(sceKernelDeleteEqueue(events.queue) == 0, "linear target completion points: event queue deletion failed");
|
||||
}
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output);
|
||||
if (!failures.empty()) {
|
||||
std::string message = "a CPU read after a point where the guest can observe a draw's completion did not see the drawn linear target:";
|
||||
for (const auto& failure : failures) message += "\n " + failure;
|
||||
Require(false, message);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("linear target completion point tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t LabelOffset = BlockBytes;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
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.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, 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, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = 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 = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + (static_cast<std::size_t>(y) * Width + x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
void Label(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t value, std::uint64_t submissions, bool watched, const char* what) {
|
||||
const auto bytes = std::as_bytes(std::span(&value, 1));
|
||||
const auto reason = device.WriteLabelOnGpu(block.Address() + LabelOffset, bytes, value, 0);
|
||||
Require(reason == 0, std::string(what) + ": the label was not recorded on the GPU (outcome " + std::to_string(reason) + ")");
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, std::string(what) + ": the drawn linear target was still pending after a label the GPU writes");
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(!watched || (recorder != nullptr && recorder->Submissions() == submissions), std::string(what) + ": storing the drawn linear target before the label waited for recorded work (" + std::to_string(recorder != nullptr ? recorder->Submissions() - submissions : 0) + " submissions)");
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff});
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
const auto red = Program(0, false);
|
||||
const auto blue = Program(1, true);
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr, "the device has no recorder");
|
||||
const bool watched = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes);
|
||||
if (!watched) std::puts("linear target completion stores: the target is outside the write watch, so a store may wait for the previous store's saved bytes; the submission checks are skipped");
|
||||
const auto pending = [&](const char* what) {
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) != nullptr, std::string(what) + ": the drawn linear target was stored before any point where its completion is observable");
|
||||
};
|
||||
Draw(device, color, red, {{0, 0}, {Width, Height}});
|
||||
pending("after the first draw");
|
||||
const auto submissions = recorder->Submissions();
|
||||
Draw(device, color, blue, {{0, 0}, {Width / 2u, Height}});
|
||||
pending("after the second draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the second draw into the linear target submitted recorded work");
|
||||
Draw(device, color, red, {{Width / 4u, Height / 4u}, {Width / 4u, Height / 2u}});
|
||||
pending("after the third draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the third draw into the linear target submitted recorded work");
|
||||
Label(device, block, 1u, submissions, watched, "the first label");
|
||||
Draw(device, color, blue, {{Width / 2u, 0}, {Width / 2u, Height}});
|
||||
pending("after the fourth draw");
|
||||
Label(device, block, 2u, submissions, watched, "the second label");
|
||||
Draw(device, color, red, {{0, Height / 2u}, {Width, Height / 2u}});
|
||||
pending("after the fifth draw");
|
||||
Require(!watched || recorder->Submissions() == submissions, "the fifth draw into the linear target submitted recorded work");
|
||||
device.WaitIdle();
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the drawn linear target was still pending after the device wait");
|
||||
std::vector<std::uint8_t> stored(TargetBytes);
|
||||
std::memcpy(stored.data(), block.data, TargetBytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const bool lastRed = y >= Height / 2u;
|
||||
const bool lastBlue = !lastRed && x >= Width / 2u;
|
||||
const bool thirdRed = !lastRed && !lastBlue && x >= Width / 4u && x < Width / 2u && y >= Height / 4u;
|
||||
const bool secondBlue = !lastRed && !lastBlue && !thirdRed;
|
||||
const float z = lastRed || thirdRed ? 0.0f : secondBlue || lastBlue ? 1.0f : -1.0f;
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), z, 1.0f};
|
||||
const auto texel = Texel(stored.data(), x, y);
|
||||
Require(texel == expected, "after the device wait, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")");
|
||||
}
|
||||
}
|
||||
std::uint32_t label = 0;
|
||||
std::memcpy(&label, block.data + LabelOffset, sizeof(label));
|
||||
Require(label == 2u, "the second label did not land: it holds " + std::to_string(label));
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device)) {
|
||||
std::puts("linear target completion store test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::puts("linear target completion store tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t RowBytes = std::size_t{Width} * 16u;
|
||||
constexpr std::uint32_t Drawn = Width / 2u;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
Block(std::size_t bytes, bool stayWatched) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!arena || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
if (arena && !stayWatched) {
|
||||
AgcDriver::GuestMemory::Unwatch(Address(), bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is still write-watched");
|
||||
}
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, std::uint32_t scissorWidth) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
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.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, 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, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = 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}, {scissorWidth, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + static_cast<std::size_t>(y) * RowBytes + static_cast<std::size_t>(x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
enum class Outcome { Kept, Lost, Skipped };
|
||||
|
||||
Outcome Run(AgcDriver::VulkanDevice& device, bool watched, bool seeded, bool hookRead) {
|
||||
const auto name = std::string(seeded ? "seeded" : "unseeded") + ", " + (watched ? "write-watched" : "unwatched") + " memory" + (hookRead ? ", driver read before the store" : "");
|
||||
Block block(watched ? std::size_t{16} * BlockBytes : BlockBytes, watched);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0xff});
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return Outcome::Skipped;
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
Draw(device, color, Program(0, false), Width);
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr && !recorder->Idle(), "the draw into an imported linear target waited for the GPU instead of being recorded");
|
||||
device.WaitIdle();
|
||||
std::vector<std::uint8_t> stored(TargetBytes);
|
||||
std::memcpy(stored.data(), block.data, TargetBytes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), 0.0f, 1.0f};
|
||||
const auto texel = Texel(stored.data(), x, y);
|
||||
Require(texel == expected, name + ": after the first draw, raw guest read: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")");
|
||||
}
|
||||
}
|
||||
if (seeded) {
|
||||
for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes, 0x5a, Drawn * 16u);
|
||||
}
|
||||
Draw(device, color, Program(1, true), Drawn);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) std::memset(block.data + y * RowBytes + Drawn * 16u, 0x42, RowBytes - Drawn * 16u);
|
||||
const bool watchedAtStore = AgcDriver::GuestMemory::Watched(block.Address(), TargetBytes);
|
||||
if (hookRead) {
|
||||
std::vector<std::uint8_t> row(RowBytes);
|
||||
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(row)), 1);
|
||||
for (std::uint32_t x = 0; x < Drawn; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), 0.0f, 1.0f, 1.0f};
|
||||
const auto texel = Texel(row.data(), x, 0);
|
||||
Require(texel == expected, name + ": a driver read of row 0 before the store: pixel (" + std::to_string(x) + ", 0) holds (" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + "), not the second draw's");
|
||||
}
|
||||
}
|
||||
device.WaitIdle();
|
||||
std::size_t wrongGpu = 0;
|
||||
std::size_t wrongCpu = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Drawn; ++x) {
|
||||
const std::array<float, 4> expected{static_cast<float>(x), static_cast<float>(y), 1.0f, 1.0f};
|
||||
if (Texel(block.data, x, y) != expected) ++wrongGpu;
|
||||
}
|
||||
auto* begin = block.data + y * RowBytes + Drawn * 16u;
|
||||
wrongCpu += static_cast<std::size_t>(std::count_if(begin, block.data + (y + 1u) * RowBytes, [](std::uint8_t byte) { return byte != 0x42u; }));
|
||||
}
|
||||
std::printf("linear target CPU writes, %s (watched at the store: %d): wrong GPU pixels %zu/%u, wrong CPU bytes %zu/%zu\n", name.c_str(), watchedAtStore ? 1 : 0, wrongGpu, Drawn * Height, wrongCpu, (RowBytes - Drawn * 16u) * Height);
|
||||
return wrongGpu == 0 && wrongCpu == 0 ? Outcome::Kept : Outcome::Lost;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
bool kept = true;
|
||||
bool ran = false;
|
||||
for (const bool watched : {true, false}) {
|
||||
for (const bool seeded : {false, true}) {
|
||||
for (const bool hookRead : {false, true}) {
|
||||
const auto outcome = Run(*device, watched, seeded, hookRead);
|
||||
ran = ran || outcome != Outcome::Skipped;
|
||||
kept = kept && outcome != Outcome::Lost;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ran) {
|
||||
std::puts("linear target CPU write test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Require(kept, "a drawn linear target's store lost the drawn half or the CPU's later stores to the other half");
|
||||
std::puts("linear target CPU write tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::size_t Pitch = std::size_t{Width} * 4u;
|
||||
constexpr std::size_t TargetBytes = Pitch * Height;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t RegionBytes = 16 * BlockBytes;
|
||||
constexpr std::size_t LabelOffset = 3 * BlockBytes;
|
||||
constexpr std::uint32_t Untouched = 0x5a5a5a5au;
|
||||
constexpr std::uint32_t Filled = 0x9abcdef0u;
|
||||
constexpr std::uint32_t Drawn = 0xff0000ffu;
|
||||
constexpr std::uint32_t LabelValue = 0x600dcafeu;
|
||||
constexpr std::uint32_t CpuStored = 0x0badf00du;
|
||||
constexpr std::uint32_t Rgba8UnormInfo = 0x28028u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t WaitEqual = 3;
|
||||
constexpr std::uint32_t WaitAtLeast = 5;
|
||||
constexpr std::array<std::array<std::uint32_t, 4>, 2> Scissors{{{16, 8, 128, 96}, {128, 32, 240, 120}}};
|
||||
constexpr std::array<std::uint32_t, 4> BlockZeroScissor{24, 8, 232, 56};
|
||||
constexpr std::size_t CpuStoreOffset = BlockBytes + 32 * Pitch + 64 * 4;
|
||||
static_assert(BlockZeroScissor[3] * Pitch <= BlockBytes);
|
||||
static_assert(CpuStoreOffset >= BlockBytes && CpuStoreOffset < TargetBytes);
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 20> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060208u, 0x7e0802f2u, 0x7e0a02ffu, 0x44800000u, 0xf80000d1u, 0x00000005u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0x7e000200u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> FillCode{
|
||||
0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xbf810000,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PairFillCode{
|
||||
0xd7460004, 0x04010c0c, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(4) std::atomic<std::uint32_t> Gate = 0;
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
|
||||
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
void AppendDraw(std::vector<std::uint32_t>& commands, std::uint64_t target, const std::array<std::uint32_t, 4>& scissor) {
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto extent = (Height << 16u) | Width;
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0x01200000}, {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, Rgba8UnormInfo},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)},
|
||||
{0x3b8, LinearAttrib3}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, extent},
|
||||
{0x81, 0x80000000}, {0x82, extent},
|
||||
{0x90, 0x80000000u | (scissor[1] << 16u) | scissor[0]}, {0x91, (scissor[3] << 16u) | scissor[2]},
|
||||
{0x94, 0x80000000}, {0x95, extent},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(2.0f * Width)}, {0x110, std::bit_cast<std::uint32_t>(2.0f * Width)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(2.0f * Height)}, {0x112, std::bit_cast<std::uint32_t>(2.0f * Height)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
{0x318, static_cast<std::uint32_t>(target >> 8u)}, {0x390, static_cast<std::uint32_t>(target >> 40u)},
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
|
||||
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
|
||||
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00b, 1u << 1u}, {0x00c, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
};
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
Append(commands, 0x79, {0x24a, 0});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(std::uint64_t address, std::size_t bytes) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(bytes / 16u), 0x4bfacu};
|
||||
}
|
||||
|
||||
void AppendDispatch(std::vector<std::uint32_t>& commands, const std::uint32_t* program, const std::vector<std::uint32_t>& userData, std::uint32_t groups) {
|
||||
const auto code = reinterpret_cast<std::uintptr_t>(program);
|
||||
Append(commands, 0x76, {0x207, 64, 1, 1});
|
||||
Append(commands, 0x76, {0x20c, static_cast<std::uint32_t>(code >> 8u), static_cast<std::uint32_t>(code >> 40u)});
|
||||
Append(commands, 0x76, {0x213, (static_cast<std::uint32_t>(userData.size()) << 1u) | 0x80u});
|
||||
commands.push_back(0xc0007600u | (static_cast<std::uint32_t>(userData.size()) << 16u));
|
||||
commands.push_back(0x240u);
|
||||
commands.insert(commands.end(), userData.begin(), userData.end());
|
||||
commands.insert(commands.end(), {0xc0031500u, groups, 1, 1, 0x41u});
|
||||
}
|
||||
|
||||
void AppendFill(std::vector<std::uint32_t>& commands, std::uint64_t target, const std::uint32_t* copied, std::size_t bytes) {
|
||||
const auto direct = BufferDescriptor(target, bytes);
|
||||
const auto other = BufferDescriptor(reinterpret_cast<std::uintptr_t>(copied), bytes);
|
||||
std::vector<std::uint32_t> userData(direct.begin(), direct.end());
|
||||
userData.insert(userData.end(), {Filled, Filled, Filled, Filled});
|
||||
userData.insert(userData.end(), other.begin(), other.end());
|
||||
AppendDispatch(commands, PairFillCode.data(), userData, static_cast<std::uint32_t>(bytes / 1024u));
|
||||
}
|
||||
|
||||
void AppendLabel(std::vector<std::uint32_t>& commands, std::uint64_t label) {
|
||||
commands.insert(commands.end(), {0xc0064900u, 0x514u, 1u << 29u, static_cast<std::uint32_t>(label), static_cast<std::uint32_t>(label >> 32u), LabelValue, 0, 0});
|
||||
}
|
||||
|
||||
void AppendGate(std::vector<std::uint32_t>& commands, std::uint32_t function, std::uint32_t reference) {
|
||||
const auto gate = reinterpret_cast<std::uintptr_t>(&Gate);
|
||||
commands.insert(commands.end(), {0xc0053c00u, 0x10u | function, static_cast<std::uint32_t>(gate), static_cast<std::uint32_t>(gate >> 32u), reference, 0xffffffffu, 0x19u});
|
||||
}
|
||||
|
||||
void Submit(std::vector<std::uint32_t>& commands) {
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
|
||||
class Region {
|
||||
public:
|
||||
explicit Region(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "linear target direct write completion: cannot map the guest region");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "linear target direct write completion: cannot allocate the guest region");
|
||||
auto* words = reinterpret_cast<std::uint32_t*>(data);
|
||||
std::fill(words, words + bytes / 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Region() {
|
||||
GuestAllocations::Mutation().Remove(data);
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Region(const Region&) = delete;
|
||||
Region& operator=(const Region&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::uint32_t Word(std::size_t offset) const { return std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).load(std::memory_order_acquire); }
|
||||
void Put(std::size_t offset, std::uint32_t value) const { std::atomic_ref<std::uint32_t>(*reinterpret_cast<std::uint32_t*>(data + offset)).store(value, std::memory_order_release); }
|
||||
std::size_t bytes;
|
||||
|
||||
private:
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
void Import(const Region& region) {
|
||||
const auto buffer = BufferDescriptor(region.Address(), region.bytes);
|
||||
std::vector<std::uint32_t> userData(buffer.begin(), buffer.end());
|
||||
userData.insert(userData.end(), {Untouched, Untouched, Untouched, Untouched});
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendDispatch(commands, FillCode.data(), userData, static_cast<std::uint32_t>(region.bytes / 1024u));
|
||||
Submit(commands);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
bool Covered(std::uint32_t x, std::uint32_t y) {
|
||||
return std::any_of(Scissors.begin(), Scissors.end(), [&](const std::array<std::uint32_t, 4>& scissor) { return x >= scissor[0] && x < scissor[2] && y >= scissor[1] && y < scissor[3]; });
|
||||
}
|
||||
|
||||
std::string Mismatches(const Region& region, bool drawsLast) {
|
||||
std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, wrongElsewhere = 0, drawnElsewhere = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto value = region.Word(y * Pitch + x * 4u);
|
||||
if (drawsLast && Covered(x, y)) {
|
||||
++covered;
|
||||
wrongCovered += value == Drawn ? 0u : 1u;
|
||||
filledCovered += value == Filled ? 1u : 0u;
|
||||
} else {
|
||||
wrongElsewhere += value == Filled ? 0u : 1u;
|
||||
drawnElsewhere += value == Drawn ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wrongCovered == 0 && wrongElsewhere == 0) return {};
|
||||
const auto elsewhere = std::to_string(std::size_t{Width} * Height - covered);
|
||||
if (!drawsLast) return std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels do not hold the fill's pattern (" + std::to_string(drawnElsewhere) + " hold the draws' color)";
|
||||
return std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draws covered do not hold their color (" + std::to_string(filledCovered) + " hold the fill's pattern) and " + std::to_string(wrongElsewhere) + " of the " + elsewhere + " pixels outside them do not hold the fill's pattern";
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string CpuStoreMismatches(const Region& region) {
|
||||
std::size_t covered = 0, wrongCovered = 0, filledCovered = 0, others = 0, wrongOthers = 0;
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto offset = y * Pitch + x * 4u;
|
||||
if (offset == CpuStoreOffset) continue;
|
||||
const auto value = region.Word(offset);
|
||||
if (x >= BlockZeroScissor[0] && x < BlockZeroScissor[2] && y >= BlockZeroScissor[1] && y < BlockZeroScissor[3]) {
|
||||
++covered;
|
||||
wrongCovered += value == Drawn ? 0u : 1u;
|
||||
filledCovered += value == Filled ? 1u : 0u;
|
||||
} else {
|
||||
++others;
|
||||
wrongOthers += value == (offset < BlockBytes ? Filled : Untouched) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<std::string> parts;
|
||||
if (wrongCovered != 0) parts.push_back(std::to_string(wrongCovered) + " of the " + std::to_string(covered) + " pixels the draw covered in block 0 do not hold its color (" + std::to_string(filledCovered) + " hold the fill's pattern)");
|
||||
if (const auto stored = region.Word(CpuStoreOffset); stored != CpuStored) parts.push_back("the CPU store in block 1 holds " + Hex(stored) + ", not " + Hex(CpuStored));
|
||||
if (wrongOthers != 0) parts.push_back(std::to_string(wrongOthers) + " of the " + std::to_string(others) + " other pixels do not hold the fill's pattern in block 0 or the untouched pattern in block 1");
|
||||
std::string text;
|
||||
for (const auto& part : parts) text += (text.empty() ? "" : "; ") + part;
|
||||
return text;
|
||||
}
|
||||
|
||||
bool Await(const std::function<bool()>& done) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!done()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) return false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class Order { FillFirst, DrawsFirst };
|
||||
|
||||
bool Run(Order order, const Region& region, std::vector<std::string>& failures) {
|
||||
const bool fillFirst = order == Order::FillFirst;
|
||||
const char* name = fillFirst ? "fill, then draws" : "draws, then fill";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
if (!AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false;
|
||||
std::vector<std::uint32_t> copied(TargetBytes / 4u + 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true);
|
||||
struct Unregister {
|
||||
void* data;
|
||||
~Unregister() { GuestAllocations::Mutation().Remove(data); }
|
||||
} unregister{copied.data()};
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
if (fillFirst) AppendFill(commands, target, copied.data(), TargetBytes);
|
||||
for (const auto& scissor : Scissors) AppendDraw(commands, target, scissor);
|
||||
if (!fillFirst) AppendFill(commands, target, copied.data(), TargetBytes);
|
||||
AppendLabel(commands, target + LabelOffset);
|
||||
if (fillFirst) AppendGate(commands, WaitEqual, 1);
|
||||
Submit(commands);
|
||||
std::string failure;
|
||||
if (fillFirst) {
|
||||
if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draws never landed";
|
||||
else if (const auto mismatches = Mismatches(region, true); !mismatches.empty()) failure = "when the label after the draws landed: " + mismatches;
|
||||
Gate.store(1, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
} else {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
if (region.Word(LabelOffset) != LabelValue) failure = "the label after the fill had not landed by the device wait";
|
||||
else if (const auto mismatches = Mismatches(region, false); !mismatches.empty()) failure = "after the label and the device wait: " + mismatches;
|
||||
}
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target direct write completion, %s: %s\n", name, failure.empty() ? "the later writer's values" : failure.c_str());
|
||||
if (!failure.empty()) failures.push_back(std::string(name) + ": " + failure);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunCpuStore(bool watched, const Region& region, std::vector<std::string>& failures) {
|
||||
const std::string name = std::string("fill, draw in block 0, CPU store in block 1, ") + (watched ? "write-watched" : "unwatched") + " memory";
|
||||
const auto target = region.Address();
|
||||
Import(region);
|
||||
if (watched && !AgcDriver::GuestMemory::Watched(target, TargetBytes)) return false;
|
||||
if (!watched) {
|
||||
AgcDriver::GuestMemory::Unwatch(target, region.bytes);
|
||||
Require(!AgcDriver::GuestMemory::Watched(target, region.bytes), "linear target direct write completion: the region is still write-watched");
|
||||
}
|
||||
std::vector<std::uint32_t> copied(TargetBytes / 4u + 4u, Untouched);
|
||||
GuestAllocations::Mutation().Add(copied.data(), TargetBytes + 16u, true, true, true);
|
||||
struct Unregister {
|
||||
void* data;
|
||||
~Unregister() { GuestAllocations::Mutation().Remove(data); }
|
||||
} unregister{copied.data()};
|
||||
Gate.store(0, std::memory_order_release);
|
||||
std::vector<std::uint32_t> commands;
|
||||
AppendFill(commands, target, copied.data(), BlockBytes);
|
||||
AppendDraw(commands, target, BlockZeroScissor);
|
||||
AppendGate(commands, WaitAtLeast, 1);
|
||||
AppendLabel(commands, target + LabelOffset);
|
||||
AppendGate(commands, WaitAtLeast, 2);
|
||||
Submit(commands);
|
||||
const auto completed = [&] { return std::atomic_ref<std::uint32_t>(copied[0]).load(std::memory_order_acquire) == Filled; };
|
||||
bool pendingAtStore = false;
|
||||
const bool recorded = Await([&] {
|
||||
std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex());
|
||||
if (AgcDriver::Graphics::StorageTexture::FindLive(target, TargetBytes) == nullptr) return false;
|
||||
pendingAtStore = !completed();
|
||||
region.Put(CpuStoreOffset, CpuStored);
|
||||
return true;
|
||||
});
|
||||
Gate.store(1, std::memory_order_release);
|
||||
std::string failure;
|
||||
bool completedAtLabel = false;
|
||||
if (!recorded) failure = "the draw into block 0 never made the target's image";
|
||||
else if (!Await([&] { return region.Word(LabelOffset) == LabelValue; })) failure = "the label after the draw never landed";
|
||||
else {
|
||||
completedAtLabel = completed();
|
||||
failure = CpuStoreMismatches(region);
|
||||
if (!failure.empty()) failure = "when the label after the draw landed: " + failure;
|
||||
if (watched && (!pendingAtStore || !completedAtLabel)) failure += std::string(failure.empty() ? "" : "; ") + (pendingAtStore ? "the fill's completion had not run when the label landed" : "the fill's completion had run before the CPU store") + ", so the ordering under test did not occur";
|
||||
}
|
||||
const bool watchedAtLabel = AgcDriver::GuestMemory::Watched(target, TargetBytes);
|
||||
Gate.store(2, std::memory_order_release);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(target, region.bytes, nullptr, "test release");
|
||||
std::printf("linear target direct write completion, %s (watched at the label: %d, completion pending at the CPU store: %d, run by the label: %d): %s\n", name.c_str(), watchedAtLabel ? 1 : 0, pendingAtStore ? 1 : 0, completedAtLabel ? 1 : 0, failure.empty() ? "the draw's color and the CPU store" : failure.c_str());
|
||||
if (!failure.empty()) failures.push_back(name + ": " + failure);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).hostImportAlignment == 0) {
|
||||
std::puts("linear target direct write completion tests skipped, host imports are unavailable on this device, so the target is never resident");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
std::vector<std::unique_ptr<Region>> regions;
|
||||
struct ShutdownGuard {
|
||||
~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} }
|
||||
} shutdown;
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
const auto unwatchedImports = [] {
|
||||
std::puts("linear target direct write completion tests skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked");
|
||||
return VulkanTestSkipped;
|
||||
};
|
||||
std::vector<std::string> failures;
|
||||
for (const auto order : {Order::FillFirst, Order::DrawsFirst}) {
|
||||
regions.push_back(std::make_unique<Region>(RegionBytes));
|
||||
if (!Run(order, *regions.back(), failures)) return unwatchedImports();
|
||||
}
|
||||
for (const bool watched : {true, false}) {
|
||||
regions.push_back(std::make_unique<Region>(RegionBytes));
|
||||
if (!RunCpuStore(watched, *regions.back(), failures)) return unwatchedImports();
|
||||
}
|
||||
if (!failures.empty()) {
|
||||
std::string message = "linear target direct write completion: at a completion point, the memory of a resident linear target did not hold the later of a direct buffer write with completion work and draws into it, or lost a CPU store made outside the draws:";
|
||||
for (const auto& failure : failures) message += "\n " + failure;
|
||||
Require(false, message);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("linear target direct write completion tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint32_t ReadX = 10;
|
||||
constexpr std::uint32_t ReadY = 6;
|
||||
constexpr std::uint32_t Drawn = 0x3f800000u;
|
||||
constexpr std::uint32_t Unwritten = 0xdeadbeefu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> CopyCode{0x7e000280u, 0xdc308000u, 0x01000000u, 0xbf8c3f70u, 0xdc708000u, 0x00020100u, 0xbf810000u};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map a guest block");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate a guest block");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "a guest block is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
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.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, 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, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = 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 = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint64_t source, std::uint64_t destination) {
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
const std::array<std::uint32_t, 4> userData{static_cast<std::uint32_t>(source), static_cast<std::uint32_t>(source >> 32u), static_cast<std::uint32_t>(destination), static_cast<std::uint32_t>(destination >> 32u)};
|
||||
const std::span<const std::uint32_t> code(CopyCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Dword(const std::uint8_t* bytes) {
|
||||
std::uint32_t value = 0;
|
||||
std::memcpy(&value, bytes, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (!context.bufferDeviceAddress) {
|
||||
std::puts("skipped, the device lacks bufferDeviceAddress");
|
||||
return false;
|
||||
}
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0});
|
||||
Block output(BlockBytes);
|
||||
for (std::size_t offset = 0; offset < output.bytes; offset += sizeof(Unwritten)) std::memcpy(output.data + offset, &Unwritten, sizeof(Unwritten));
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) {
|
||||
std::puts("skipped, host imports are unavailable");
|
||||
return false;
|
||||
}
|
||||
const auto color = DecodeTarget(block);
|
||||
Require(color.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && color.bytes == TargetBytes, "the target is not a linear R32G32B32A32 target");
|
||||
const auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr, "the device has no recorder");
|
||||
Draw(device, color, Program(0, false), {{0, 0}, {Width, Height}});
|
||||
device.WaitIdle();
|
||||
Require(StorageTexture::FindPending(block.Address(), TargetBytes) == nullptr, "the linear target was still pending after the device wait");
|
||||
const auto blueOffset = (static_cast<std::size_t>(ReadY) * Width + ReadX) * 16u + 8u;
|
||||
Require(Dword(block.data + blueOffset) == 0u, "the first draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")");
|
||||
Draw(device, color, Program(1, true), {{8, 4}, {16, 8}});
|
||||
Require(recorder->Recording(), "the draw into the linear target was not left in the open batch");
|
||||
Dispatch(device, block.Address() + blueOffset, output.Address());
|
||||
device.WaitIdle();
|
||||
const auto read = Dword(output.data);
|
||||
Require(read == Drawn, "the address-based dispatch recorded after the draw read " + Hex(read) + " from the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + "), not the drawn " + Hex(Drawn));
|
||||
Require(Dword(block.data + blueOffset) == Drawn, "the second draw left " + Hex(Dword(block.data + blueOffset)) + " in the blue channel of pixel (" + std::to_string(ReadX) + ", " + std::to_string(ReadY) + ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device)) return VulkanTestSkipped;
|
||||
std::puts("linear target dispatch access tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::Graphics::StorageTexture;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t TargetBytes = std::size_t{Width} * Height * 16u;
|
||||
constexpr std::size_t CopyTargetOffset = 0x40000;
|
||||
constexpr std::size_t FirstSourceOffset = 0x80000;
|
||||
constexpr std::size_t SecondSourceOffset = 0x90000;
|
||||
constexpr std::size_t PointedOffset = 0xa0000;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint32_t FirstSource = 0x3e800000u;
|
||||
constexpr std::uint32_t SecondSource = 0x3f400000u;
|
||||
constexpr std::uint32_t Pointed = 0x3f800000u;
|
||||
constexpr std::uint32_t Unwritten = 0xdeadbeefu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> PixelOpaque{0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> PixelCopy{0x7e040280u, 0xe0301000u, 0x80010102u, 0xbf8c3f70u, 0xe0701000u, 0x80020102u, 0x7e100301u, 0x7e120280u, 0x7e140280u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 4> FirstOutput{};
|
||||
alignas(256) std::array<std::uint32_t, 4> SecondOutput{};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> RawBuffer(std::uint64_t address, std::uint32_t bytes) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color targets");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color targets");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color targets are not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::span<const std::uint32_t> pixelCode, std::span<const std::uint32_t> pixelUserData, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
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.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, 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, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = 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 = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 12> CopyUserData(std::uint64_t source, const std::array<std::uint32_t, 4>& output) {
|
||||
std::array<std::uint32_t, 12> userData{};
|
||||
const auto read = RawBuffer(source, 16u);
|
||||
const auto written = RawBuffer(reinterpret_cast<std::uintptr_t>(output.data()), 16u);
|
||||
std::copy(read.begin(), read.end(), userData.begin() + 4);
|
||||
std::copy(written.begin(), written.end(), userData.begin() + 8);
|
||||
return userData;
|
||||
}
|
||||
|
||||
std::array<float, 4> Texel(const std::uint8_t* target, std::uint32_t x, std::uint32_t y) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), target + (static_cast<std::size_t>(y) * Width + x) * 16u, sizeof(texel));
|
||||
return texel;
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string Describe(const std::array<float, 4>& texel) {
|
||||
return "(" + std::to_string(texel[0]) + ", " + std::to_string(texel[1]) + ", " + std::to_string(texel[2]) + ", " + std::to_string(texel[3]) + ")";
|
||||
}
|
||||
|
||||
void ExpectTexels(const std::uint8_t* target, const char* what, std::uint32_t firstX, std::uint32_t lastX, const std::array<float, 4>& expected) {
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = firstX; x < lastX; ++x) {
|
||||
const auto texel = Texel(target, x, y);
|
||||
Require(texel == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") holds " + Describe(texel) + ", expected " + Describe(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device, bool unwatch) {
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (!context.bufferDeviceAddress) {
|
||||
std::puts("skipped, the device lacks bufferDeviceAddress");
|
||||
return false;
|
||||
}
|
||||
Block block(16 * BlockBytes);
|
||||
std::fill(block.data, block.data + block.bytes, std::uint8_t{0});
|
||||
const auto put = [&](std::size_t offset, std::uint32_t value, std::size_t dwords) {
|
||||
for (std::size_t dword = 0; dword < dwords; ++dword) std::memcpy(block.data + offset + dword * 4u, &value, sizeof(value));
|
||||
};
|
||||
put(FirstSourceOffset, FirstSource, 4);
|
||||
put(SecondSourceOffset, SecondSource, 4);
|
||||
put(PointedOffset, Pointed, 1);
|
||||
FirstOutput.fill(Unwritten);
|
||||
SecondOutput.fill(Unwritten);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) {
|
||||
std::puts("skipped, host imports are unavailable");
|
||||
return false;
|
||||
}
|
||||
const auto drawn = DecodeTarget(block.Address());
|
||||
const auto copied = DecodeTarget(block.Address() + CopyTargetOffset);
|
||||
Require(drawn.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && drawn.bytes == TargetBytes && copied.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && copied.bytes == TargetBytes, "the targets are not linear R32G32B32A32 targets");
|
||||
auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr && context.descriptorCache != nullptr, "the device has no recorder or descriptor cache");
|
||||
const auto pending = [](std::uint64_t address) { return StorageTexture::FindPending(address, TargetBytes) != nullptr; };
|
||||
const VkRect2D whole{{0, 0}, {Width, Height}};
|
||||
Draw(device, drawn, PixelOpaque, {}, whole);
|
||||
Draw(device, copied, PixelOpaque, {}, whole);
|
||||
device.WaitIdle();
|
||||
Require(!pending(drawn.address) && !pending(copied.address), "a linear target was still pending after the device wait");
|
||||
if (unwatch) AgcDriver::GuestMemory::Unwatch(block.Address(), block.bytes);
|
||||
const bool watched = AgcDriver::GuestMemory::Watched(copied.address, TargetBytes);
|
||||
Require(!unwatch || !watched, "the copied target is still write-watched after its block was taken out of the write watch");
|
||||
const auto submitted = recorder->Submissions();
|
||||
const auto pointed = block.Address() + PointedOffset;
|
||||
const std::array<std::uint32_t, 4> pointer{0u, 0u, static_cast<std::uint32_t>(pointed), static_cast<std::uint32_t>(pointed >> 32u)};
|
||||
Draw(device, drawn, PixelRead, pointer, whole);
|
||||
Require(pending(drawn.address), "the address-based draw did not leave its linear target pending");
|
||||
Require(recorder->Recording() && recorder->Submissions() == submitted, "the address-based draw did not stay in the open batch");
|
||||
const auto setsBefore = context.descriptorCache->Counters().sets;
|
||||
const auto firstCopy = CopyUserData(block.Address() + FirstSourceOffset, FirstOutput);
|
||||
Draw(device, copied, PixelCopy, firstCopy, {{0, 0}, {Width / 2u, Height}});
|
||||
Require(!pending(drawn.address), "the draw writing a copied buffer did not store the drawn linear target first");
|
||||
Require(pending(copied.address), "the first copying draw did not leave its linear target pending");
|
||||
Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "storing the drawn linear target did not submit the batch the address-based draw read it in (" + std::to_string(recorder->Submissions() - submitted) + " submissions)");
|
||||
const auto setsBetween = context.descriptorCache->Counters().sets;
|
||||
const auto secondCopy = CopyUserData(block.Address() + SecondSourceOffset, SecondOutput);
|
||||
Draw(device, copied, PixelCopy, secondCopy, {{static_cast<std::int32_t>(Width / 2u), 0}, {Width / 2u, Height}});
|
||||
if (watched) Require(recorder->Recording() && recorder->Submissions() == submitted + 1, "the second copying draw submitted recorded work");
|
||||
else Require(recorder->Submissions() == submitted + 2, "the second copying draw into a stored target outside the write watch did not submit once to capture its bytes (" + std::to_string(recorder->Submissions() - submitted - 1) + " submissions)");
|
||||
const auto setsAfter = context.descriptorCache->Counters().sets;
|
||||
device.WaitIdle();
|
||||
Require(FirstOutput[0] == FirstSource, "the first copying draw's output holds " + Hex(FirstOutput[0]) + ", not its source's " + Hex(FirstSource));
|
||||
Require(SecondOutput[0] == SecondSource, "the second copying draw's output holds " + Hex(SecondOutput[0]) + ", not its source's " + Hex(SecondSource));
|
||||
ExpectTexels(block.data + CopyTargetOffset, "the first copying draw's half", 0, Width / 2u, {0.25f, 0.0f, 0.0f, 1.0f});
|
||||
ExpectTexels(block.data + CopyTargetOffset, "the second copying draw's half", Width / 2u, Width, {0.75f, 0.0f, 0.0f, 1.0f});
|
||||
ExpectTexels(block.data, "the address-based draw's target", 0, Width, {1.0f, 0.0f, 0.0f, 0.0f});
|
||||
Require(setsAfter - setsBetween == setsBetween - setsBefore, "the second copying draw allocated " + std::to_string(setsAfter - setsBetween) + " descriptor sets, the first " + std::to_string(setsBetween - setsBefore));
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_RELEASE_ON_UNLOCK", "1");
|
||||
#else
|
||||
setenv("APS5_RELEASE_ON_UNLOCK", "1", 1);
|
||||
#endif
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device, false)) return VulkanTestSkipped;
|
||||
if (!Expect(*device, true)) return VulkanTestSkipped;
|
||||
std::puts("linear target draw binding tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t RegionBytes = 8 * BlockBytes;
|
||||
constexpr std::uint32_t Rgba32FloatInfo = 0x8738u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32AbgrExport = 9;
|
||||
constexpr std::uint8_t Initial = 0x3f;
|
||||
constexpr std::uint8_t Sentinel = 0x37;
|
||||
constexpr std::uint8_t Gap = 0x29;
|
||||
constexpr std::size_t GapBytes = 0x100;
|
||||
|
||||
struct Surface {
|
||||
std::size_t offset;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::size_t RowBytes() const { return std::size_t{width} * 16u; }
|
||||
std::size_t Bytes() const { return RowBytes() * height; }
|
||||
};
|
||||
|
||||
constexpr Surface Tall{0, 64, 256};
|
||||
constexpr Surface Shifted{6 * BlockBytes + GapBytes, 64, 16};
|
||||
constexpr VkRect2D Seed{{63, 255}, {1, 1}};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 16>, 2> Programs{};
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(std::size_t index, bool blue) {
|
||||
auto& code = Programs.at(index);
|
||||
std::size_t at = 0;
|
||||
code[at++] = 0x36000000u | (20u << 17u) | 0xffu;
|
||||
code[at++] = 0x0000ffffu;
|
||||
code[at++] = 0x2c000000u | (21u << 17u) | 0x90u;
|
||||
code[at++] = 0x7e000c00u | (20u << 17u) | (256u + 20u);
|
||||
code[at++] = 0x7e000c00u | (21u << 17u) | (256u + 21u);
|
||||
code[at++] = (blue ? 0x7e0002f2u : 0x7e000280u) | (22u << 17u);
|
||||
code[at++] = 0x7e0002f2u | (23u << 17u);
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x17161514u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), arena(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color targets");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color targets");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (arena) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (arena) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool arena;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, const Surface& surface) {
|
||||
const auto address = block.Address() + surface.offset;
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(address >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = Rgba32FloatInfo;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x323] = 0;
|
||||
cx[0x324] = 0;
|
||||
cx[0x3b0] = ((surface.width - 1u) << 14u) | (surface.height - 1u);
|
||||
cx[0x3b8] = LinearAttrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(address >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
color.exportIndex = 0;
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const Surface& surface, std::span<const std::uint32_t> pixelCode, VkRect2D scissor) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto vertexBuffer = BufferDescriptor(Triangle.data(), 16u, static_cast<std::uint32_t>(Triangle.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::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
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.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionFixedPoint);
|
||||
pixel.targetOutputMode.at(0) = Float32AbgrExport;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(pixelCode.data()), std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}},
|
||||
{waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, 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, target)}
|
||||
}};
|
||||
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.blendEnable = VK_FALSE;
|
||||
blend.colorWriteMask = 0xf;
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {surface.width, surface.height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(surface.height), static_cast<float>(surface.width), -static_cast<float>(surface.height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = scissor;
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
bool Inside(const VkRect2D& rect, std::uint32_t x, std::uint32_t y) {
|
||||
return x >= static_cast<std::uint32_t>(rect.offset.x) && x < static_cast<std::uint32_t>(rect.offset.x) + rect.extent.width && y >= static_cast<std::uint32_t>(rect.offset.y) && y < static_cast<std::uint32_t>(rect.offset.y) + rect.extent.height;
|
||||
}
|
||||
|
||||
std::uint8_t* Pixel(const Block& block, const Surface& surface, std::uint32_t x, std::uint32_t y) {
|
||||
return block.data + surface.offset + static_cast<std::size_t>(y) * surface.RowBytes() + static_cast<std::size_t>(x) * 16u;
|
||||
}
|
||||
|
||||
bool Holds(const std::uint8_t* pixel, std::uint8_t value) {
|
||||
return std::all_of(pixel, pixel + 16, [&](std::uint8_t byte) { return byte == value; });
|
||||
}
|
||||
|
||||
bool HoldsOutput(const std::uint8_t* pixel, std::uint32_t x, std::uint32_t y, float z) {
|
||||
std::array<float, 4> texel{};
|
||||
std::memcpy(texel.data(), pixel, sizeof(texel));
|
||||
return texel == std::array<float, 4>{static_cast<float>(x), static_cast<float>(y), z, 1.0f};
|
||||
}
|
||||
|
||||
bool Expect(AgcDriver::VulkanDevice& device) {
|
||||
Block block(RegionBytes);
|
||||
std::fill(block.data, block.data + block.bytes, Initial);
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto tall = DecodeTarget(block, Tall);
|
||||
const auto shifted = DecodeTarget(block, Shifted);
|
||||
Require(tall.tileMode == AgcDriver::Graphics::ColorTileMode::Linear && tall.bytes == Tall.Bytes() && shifted.bytes == Shifted.Bytes(), "the targets are not linear R32G32B32A32 targets");
|
||||
constexpr VkRect2D first{{8, 4}, {16, 16}};
|
||||
constexpr VkRect2D second{{16, 12}, {24, 16}};
|
||||
constexpr VkRect2D whole{{0, 0}, {Shifted.width, Shifted.height}};
|
||||
Draw(device, tall, Tall, Program(0, false), Seed);
|
||||
device.WaitIdle();
|
||||
Draw(device, tall, Tall, Program(0, false), first);
|
||||
Draw(device, tall, Tall, Program(1, true), second);
|
||||
Draw(device, shifted, Shifted, Program(0, false), whole);
|
||||
const std::array<std::array<std::uint32_t, 2>, 3> sentinels{{{50, 10}, {20, 40}, {30, 200}}};
|
||||
const std::array<std::array<std::uint32_t, 2>, 2> between{{{30, 6}, {10, 25}}};
|
||||
for (const auto& [x, y] : sentinels) std::memset(Pixel(block, Tall, x, y), Sentinel, 16);
|
||||
for (const auto& [x, y] : between) std::memset(Pixel(block, Tall, x, y), Sentinel, 16);
|
||||
Require((block.Address() + Shifted.offset) / BlockBytes * BlockBytes == block.Address() + Shifted.offset - GapBytes, "the shifted target does not start past a 64 KiB boundary");
|
||||
std::memset(block.data + Shifted.offset - GapBytes, Gap, GapBytes);
|
||||
device.WaitIdle();
|
||||
std::size_t wrongDrawn = 0, wrongSentinels = 0, wrongBetween = 0, wrongUntouched = 0, wrongShifted = 0;
|
||||
for (std::uint32_t y = 0; y < Tall.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Tall.width; ++x) {
|
||||
const auto* pixel = Pixel(block, Tall, x, y);
|
||||
const auto at = [&](const auto& point) { return point[0] == x && point[1] == y; };
|
||||
if (std::any_of(sentinels.begin(), sentinels.end(), at)) wrongSentinels += Holds(pixel, Sentinel) ? 0u : 1u;
|
||||
else if (std::any_of(between.begin(), between.end(), at)) wrongBetween += Holds(pixel, Sentinel) ? 0u : 1u;
|
||||
else if (Inside(second, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 1.0f) ? 0u : 1u;
|
||||
else if (Inside(first, x, y) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u;
|
||||
else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
for (std::uint32_t y = 0; y < Shifted.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Shifted.width; ++x) wrongShifted += HoldsOutput(Pixel(block, Shifted, x, y), x, y, 0.0f) ? 0u : 1u;
|
||||
}
|
||||
const auto gapLost = static_cast<std::size_t>(std::count_if(block.data + Shifted.offset - GapBytes, block.data + Shifted.offset, [](std::uint8_t byte) { return byte != Gap; }));
|
||||
std::printf("linear target rectangle bound: wrong drawn pixels %zu, lost sentinels %zu/%zu, lost sentinels between the rectangles %zu/%zu, changed untouched pixels %zu, wrong shifted-target pixels %zu, lost gap bytes %zu/%zu\n", wrongDrawn, wrongSentinels, sentinels.size(), wrongBetween, between.size(), wrongUntouched, wrongShifted, gapLost, GapBytes);
|
||||
Require(wrongDrawn == 0 && wrongShifted == 0, "a drawn linear target's store lost pixels its draws wrote");
|
||||
Require(wrongSentinels == 0 && gapLost == 0, "a drawn linear target's store overwrote CPU stores outside the drawn rectangles");
|
||||
Require(wrongBetween == 0, "a drawn linear target's store overwrote CPU stores between two draws' scissor rectangles");
|
||||
Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no draw and no CPU store wrote");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ExpectManyRows(AgcDriver::VulkanDevice& device) {
|
||||
Block block(RegionBytes);
|
||||
std::fill(block.data, block.data + block.bytes, Initial);
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
|
||||
if (AgcDriver::Graphics::HostImportFor(context, block.Address(), block.bytes) == nullptr) return false;
|
||||
const auto tall = DecodeTarget(block, Tall);
|
||||
constexpr std::uint32_t Rows = 17;
|
||||
constexpr std::uint32_t Spacing = 15;
|
||||
Draw(device, tall, Tall, Program(0, false), Seed);
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t row = 0; row < Rows; ++row) Draw(device, tall, Tall, Program(0, false), VkRect2D{{8, static_cast<std::int32_t>(row * Spacing)}, {16, 1}});
|
||||
constexpr std::array<std::uint32_t, 2> between{10, 70};
|
||||
std::memset(Pixel(block, Tall, between[0], between[1]), Sentinel, 16);
|
||||
device.WaitIdle();
|
||||
std::size_t wrongDrawn = 0, wrongUntouched = 0;
|
||||
for (std::uint32_t y = 0; y < Tall.height; ++y) {
|
||||
const bool drawnRow = y % Spacing == 0 && y / Spacing < Rows;
|
||||
for (std::uint32_t x = 0; x < Tall.width; ++x) {
|
||||
if (x == between[0] && y == between[1]) continue;
|
||||
const auto* pixel = Pixel(block, Tall, x, y);
|
||||
if ((drawnRow && x >= 8 && x < 24) || Inside(Seed, x, y)) wrongDrawn += HoldsOutput(pixel, x, y, 0.0f) ? 0u : 1u;
|
||||
else wrongUntouched += Holds(pixel, Initial) ? 0u : 1u;
|
||||
}
|
||||
}
|
||||
const bool kept = Holds(Pixel(block, Tall, between[0], between[1]), Sentinel);
|
||||
std::printf("linear target rectangle bound, %u one-row scissors: wrong drawn pixels %zu, sentinel between the rows %s, changed untouched pixels %zu\n", Rows, wrongDrawn, kept ? "kept" : "lost", wrongUntouched);
|
||||
Require(wrongDrawn == 0, "a drawn linear target's store lost pixels its one-row draws wrote");
|
||||
Require(kept, "a drawn linear target's store overwrote a CPU store between two one-row scissors");
|
||||
Require(wrongUntouched == 0, "a drawn linear target's store changed pixels no one-row draw and no CPU store wrote");
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!Expect(*device) || !ExpectManyRows(*device)) {
|
||||
std::puts("linear target rectangle bound test skipped: no host import on this device");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::puts("linear target rectangle bound tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,9 @@
|
||||
- [`APS5_THREAD_PRIORITY=rt`](../../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc/<pid>/limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy
|
||||
- On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it.
|
||||
- [Draw input snapshots](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`.
|
||||
- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default.
|
||||
- [Pending storage-image results](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver), except drawn resident linear targets (next entry), are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). A title's own CPU reads are not seen either, so a title that reads the results after waiting for a label reads stale memory. Explicit-size file writes (`write`, `pwrite`, `writev`, `pwritev`, AIO and `fwrite`) synchronize their source ranges through the [host-read hook](../../../core/libs/prx/libc/src/GuestArena.cpp); other host readers are not covered. `APS5_STORE_AT_FLIP=1` stores every pending result at each flip and `APS5_STORE_AT_LABEL=1` before each label the GPU writes; both store every pending image, not only the ones the waited-for work wrote, and neither is on by default.
|
||||
- [Resident linear color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store inside a drawn scissor rectangle, on a pixel no primitive wrote, made before the target's store is overwritten by it, where the console keeps it. Recorded draws' results are stored before the graphics queue's next flip, label, end-of-pipe interrupt, drain or device wait, as the union of their scissor rectangles; a draw that brings a 17th rectangle that does not merge stores the target at once. A target with an attachment proxy, several mips or layers, a 3D or DCC surface, or results of a dispatch, fill or copy is stored after each draw. One that a fill or copy writes after its draws is stored whole at the next of those points, and GPU results in its CPU-written 64 KiB blocks are dropped. A store the draw pipeline's committer makes (label, end-of-pipe interrupt, device wait) skips a target whose range a queued draw still reads (`DrawPipeline::HeldReads`).
|
||||
- [Direct writes over a drawn linear target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a CPU store into a 64 KiB block of a drawn resident linear target that lands while the completion of a direct write recorded before the draws checks and stamps that block (`StampKeepingNewerResults`) drops the draws' results in that block, where the console keeps them: the stamp's own collect records the store per page only, so the store reads the block as written by the driver.
|
||||
- [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail.
|
||||
- [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too.
|
||||
- [Storage images over uncommitted guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a storage image whose guest range has no committed page when it is created throws in the `StorageTexture` constructor, which aborts the guest. Several titles reach this intermittently in runs of a few minutes, on AMD and NVIDIA alike; in two of the three logs, lines about storage images over released or rewritten memory (`memory no longer registered`, `guest memory changed while GPU results were pending`) come shortly before it. The log does not name the image's address. What the console does with a storage descriptor over memory that is not committed is unknown, and which allocation the guest recreates there was not found.
|
||||
|
||||
Reference in new issue
Block a user