Compare commits

...

11 Commits

Author SHA1 Message Date
bigjjmoneyman 0685bffb2f texture cache: convert D16<->D24/D32 depth copies instead of crashing (#1873)
gpu: convert guest D16 depth between native D16 and stenciled four-byte host depth

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 10:41:22 +01:00
nmzik a5b3673a39 graphics: draw patched-offset indirect draws from GPU-read records 2026-10-27 10:13:15 +01:00
nmzik 09753bf8ff memory: change defaults 2026-10-27 10:13:15 +01:00
nmzik 9d8c458535 gpu: keep reused smaller image tables resident and drop them a frame after their last draw 2026-10-27 10:13:15 +01:00
Mr. Big Britches 358333125d libFont: ship Noto Sans as the fallback font (#1917)
* cmake: download Noto Sans at configure time

Fetch the hinted Regular, Bold, Italic and Bold Italic of Noto Sans
v2.015 and their licence from the Noto project's font distribution
(notofonts.github.io) with FetchContent, like the other third-party
dependencies. The commit and each file's SHA-256 are pinned in
variables.

Only these four styles are needed, so they are fetched individually
instead of through the release archive (117 MB, every style and build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmake: ship Noto Sans beside the emulator

Copy the downloaded fonts to assets/fonts next to kyty_emulator on every
build, like the touch cursors and the trophy sound, and install them
there with the SIL Open Font License under licenses/noto-sans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* libFont: fall back to the shipped Noto Sans instead of Tracy's Roboto

Fonts stb_truetype cannot read, including every system font set opened
with sceFontOpenFontSet, used Roboto from Tracy's source folder, found
relative to the source tree or the working directory. Installed builds
have neither, so their system fonts drew every character as '?'.

Load Noto Sans Regular, Bold, Italic or Bold Italic from assets/fonts
next to the emulator instead, and log when a file is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* libFont: fetch Noto Sans with file(DOWNLOAD) and drop the style substitution

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-27 07:51:24 +01:00
nmzik fd6b28c4b2 build: target x86-64-v3 without floating-point contraction 2026-10-27 07:04:38 +01:00
Senaxx 6cee2dc0bd shader: inline the hot IR operand accessors 2026-10-27 07:04:38 +01:00
Senaxx a526a8b9c9 shader: hand IR uses over at once on replacement and skip use lists at program teardown 2026-10-27 07:04:38 +01:00
Senaxx 1ec69f8e7d gpu: make cull mode and front face dynamic state 2026-10-27 07:04:38 +01:00
nmzik 2463e3aaf0 gpu: write end-of-pipe labels beside GPU-written bytes to the backing store 2026-10-27 07:04:38 +01:00
nmzik 556538bed5 gpu: write D16 demotion results with one 16-bit store
Co-authored-by: strxno <rokas.pettersen@gmail.com>
2026-10-27 07:04:38 +01:00
40 changed files with 706 additions and 320 deletions
+22
View File
@@ -42,6 +42,28 @@ if(NOT KYTY_CLANG_CL)
endif()
install(FILES "${openxr_SOURCE_DIR}/LICENSE" DESTINATION "licenses/openxr")
# Noto Sans v2.015 (SIL Open Font License 1.1) is libFont's fallback font.
set(KYTY_NOTO_SANS_REPOSITORY "https://raw.githubusercontent.com/notofonts/notofonts.github.io")
set(KYTY_NOTO_SANS_COMMIT 42af67ccd12bc0e31ed85384bf73e39248265374)
set(KYTY_NOTO_SANS_BASE_URL "${KYTY_NOTO_SANS_REPOSITORY}/${KYTY_NOTO_SANS_COMMIT}/fonts")
set(KYTY_NOTO_SANS_DIR "${FETCHCONTENT_BASE_DIR}/noto-sans")
function(kyty_fetch_noto_sans path sha256)
get_filename_component(name "${path}" NAME)
file(DOWNLOAD "${KYTY_NOTO_SANS_BASE_URL}/${path}" "${KYTY_NOTO_SANS_DIR}/${name}"
EXPECTED_HASH SHA256=${sha256})
endfunction()
kyty_fetch_noto_sans(LICENSE f2095b08bed08b23a6fe26112fcd679a2bee3f002eef077eb05d215ed1051bd8)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Regular.ttf
478c558ea716033cd60c03438f628dfa75694dcf6b5f6d505a2f05fd2b4f3823)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Bold.ttf
1df075a380fc7cb898acf64c1f7b3b4dd780de3caa860178bf929de35817a913)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-Italic.ttf
467e3f89eeca4108bb8710a2b9e0cf2281ac56d5b0609211a83776d0505eecb5)
kyty_fetch_noto_sans(NotoSans/hinted/ttf/NotoSans-BoldItalic.ttf
1b602a9d6353be42c91df097a4857b69fa2696f26703d7a33b54a15d87c2622c)
install(FILES "${KYTY_NOTO_SANS_DIR}/LICENSE" DESTINATION "licenses/noto-sans")
set(KYTY_NOTO_SANS_DIR "${KYTY_NOTO_SANS_DIR}" PARENT_SCOPE)
# The guest instruction patcher needs a decoder/encoder and a small
# trampoline assembler. Pin the same revisions used by shadPS4's implementation.
set(XBYAK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+17
View File
@@ -407,6 +407,8 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(build_flags_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/BuildFlagsTests.cpp")
add_executable(page_manager_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PageManagerTests.cpp"
"${KYTY_SOURCE_DIR}/graphics/host_gpu/pageManager.cpp"
@@ -650,6 +652,7 @@ if(BUILD_TESTING)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
add_test(NAME build_flags COMMAND $<TARGET_FILE:build_flags_tests>)
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
@@ -743,6 +746,7 @@ if(BUILD_TESTING)
image_page_table_tests
memory_tracker_tests
page_manager_tests
build_flags_tests
archive_file_tests
trophy_system_tests
avplayer_file_tests
@@ -811,6 +815,19 @@ add_custom_target(kyty_touch_cursors
add_dependencies(kyty_emulator kyty_touch_cursors)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/assets/cursors" DESTINATION assets)
set(KYTY_NOTO_SANS_FONTS
"${KYTY_NOTO_SANS_DIR}/NotoSans-Regular.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-Bold.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-Italic.ttf"
"${KYTY_NOTO_SANS_DIR}/NotoSans-BoldItalic.ttf")
add_custom_target(kyty_noto_sans_fonts
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${KYTY_NOTO_SANS_FONTS}
"$<TARGET_FILE_DIR:kyty_emulator>/assets/fonts"
VERBATIM)
add_dependencies(kyty_emulator kyty_noto_sans_fonts)
install(FILES ${KYTY_NOTO_SANS_FONTS} DESTINATION assets/fonts)
target_link_libraries(kyty_emulator ${kyty_emulator_link_libraries})
if (WIN32)
target_link_libraries(kyty_emulator opengl32)
-4
View File
@@ -99,10 +99,6 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
if (Common::File::IsFileExisting(param_json)) {
Loader::SystemContentLoadParamSfo(param_json);
if (const auto flexible_memory_size = Loader::SystemContentGetFlexibleMemorySize();
flexible_memory_size != 0) {
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
}
}
if (cfg.splash_enabled) {
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
+40 -9
View File
@@ -814,6 +814,27 @@ static void PatchIndirectDrawOffsets(CommandProcessor& cp, IndirectDrawRegisters
write(registers.draw_index, draw_index);
}
// Writes the GS user-data slots the packet patches; false when the shaders cannot read the
// offsets from GPU records: a first-index or subdraw-index destination, or another stage's
// user data.
static bool IndirectDrawOffsets(IndirectDrawRegisters registers, ShaderIndirectOffsets& offsets) {
if (registers.index_offset != Pm4::SH_NOP || registers.draw_index != Pm4::SH_NOP) {
return false;
}
for (const auto [location, slot]: {std::pair {registers.vertex_offset, &offsets.vertex},
std::pair {registers.instance_offset, &offsets.instance}}) {
if (location == Pm4::SH_NOP) {
continue;
}
const uint32_t index = location - Pm4::SPI_SHADER_USER_DATA_GS_0;
if (index >= HW::UserSgprInfo::SGPRS_MAX) {
return false;
}
*slot = static_cast<uint8_t>(index);
}
return true;
}
void CommandProcessor::DrawIndirect(uint32_t data_offset, IndirectDrawRegisters registers,
uint32_t draw_initiator, bool indexed) {
const auto args_size =
@@ -845,13 +866,11 @@ void CommandProcessor::DrawIndirectMulti(uint32_t data_offset, uint32_t max_coun
// NUM_INSTANCES keeps the last draw's instance count.
m_num_instances_addr = args_addr + static_cast<uint64_t>(draw_count - 1) * stride_in_bytes +
offsetof(vk::DrawIndirectCommand, instanceCount);
// With no SGPRs to patch, the GPU reads the records if the shaders allow. Shaders apply
// the records' offsets only through those SGPRs; Vulkan applies them, which matches
// while they are zero.
const DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (registers.vertex_offset == Pm4::SH_NOP && registers.instance_offset == Pm4::SH_NOP &&
registers.index_offset == Pm4::SH_NOP && registers.draw_index == Pm4::SH_NOP &&
(!indexed || m_index_buffer_size != 0) &&
// The GPU reads the records if the shaders allow; the VS prolog loads the patched SGPRs from
// Vulkan's bases. With no SGPR patched, Vulkan applies the offsets, which matches while they
// are zero.
DrawIndirectArgs records {args_addr, draw_count, stride_in_bytes};
if (IndirectDrawOffsets(registers, records.offsets) && (!indexed || m_index_buffer_size != 0) &&
(indexed ? executor.DrawIndex(m_submit_id, CurrentBuffer(),
{.index_count = m_index_buffer_size,
.index_addr = IndexAddress(0),
@@ -976,16 +995,28 @@ void CommandProcessor::ReleaseMem(uint32_t data_sel, uint32_t interrupt_selector
// Dumped occlusion counters are written before any end-of-pipe signal reaches the guest.
m_renderer.GetOcclusionQueries().Publish();
// A label beside GPU-written bytes goes to the backing store, so they are not downloaded.
// Other labels go through the guest mapping: its fault keeps a clean cached copy current and
// downloads GPU-written bytes under the label before the store.
const auto write_label = [this, address](const void* value, uint64_t size) {
const auto vaddr = reinterpret_cast<uint64_t>(address);
auto& buffers = m_renderer.GetBufferCache();
if (!GuestRange {vaddr, size}.Valid() || !buffers.IsRegionGpuModified(vaddr, size) ||
buffers.HasGpuDirtyBytes(vaddr, size) ||
!LibKernel::Memory::TryWriteBacking(vaddr, value, size)) {
std::memcpy(address, value, size);
}
};
// EndOfPipeData: kNone, kImmediate32b, kImmediate64b, kRefClk; 5 reads GDS.
switch (data_sel) {
case 0: break;
case 1: {
const auto value = static_cast<uint32_t>(data);
std::memcpy(address, &value, sizeof(value));
write_label(&value, sizeof(value));
break;
}
case 3: data = Sync::ReadReferenceClock(); [[fallthrough]];
case 2: std::memcpy(address, &data, sizeof(data)); break;
case 2: write_label(&data, sizeof(data)); break;
case 5: {
GetScheduler().Finish();
const auto gds = static_cast<uint32_t>(data);
+30 -30
View File
@@ -488,16 +488,15 @@ void TextureCache::RefreshCopySource(ImageId id) {
}
bool TextureCache::CopyD16(Image& destination, Image& source) {
const bool source_depth = source.info.IsDepth();
const bool destination_depth = destination.info.IsDepth();
if (source_depth == destination_depth) {
// Converts guest D16 between a four-byte host depth aspect and two-byte color or D16 depth.
const bool source_wide = DepthAspectTransferBytes(source.backing.format) == sizeof(uint32_t);
if (source_wide == (DepthAspectTransferBytes(destination.backing.format) == sizeof(uint32_t))) {
return false;
}
auto& depth = source_depth ? source : destination;
auto& color = source_depth ? destination : source;
const auto transfer_bytes = DepthAspectTransferBytes(depth.backing.format);
auto& depth = source_wide ? source : destination;
auto& narrow = source_wide ? destination : source;
if (depth.info.bytes_per_block != sizeof(uint16_t) ||
color.info.bytes_per_block != sizeof(uint16_t) || transfer_bytes != sizeof(uint32_t)) {
narrow.info.bytes_per_block != sizeof(uint16_t)) {
return false;
}
EXIT_IF(source.backing.samples != 1 || destination.backing.samples != 1 ||
@@ -507,42 +506,42 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
const auto layers = depth.info.resources.layers;
const uint64_t depth_slice =
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * transfer_bytes;
const uint64_t color_slice =
static_cast<uint64_t>(color.info.pitch) * color.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
const auto depth_size = depth_slice * layers;
const auto color_size = color_slice * layers;
const std::array depth_copies {
static_cast<uint64_t>(depth.info.pitch) * depth.info.extent.height * sizeof(uint32_t);
const uint64_t narrow_slice =
static_cast<uint64_t>(narrow.info.pitch) * narrow.info.extent.height * sizeof(uint16_t);
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || narrow_slice > UINT64_MAX / layers);
const std::array depth_copies {
vk::BufferImageCopy {0,
depth.info.pitch,
depth.info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
depth.info.extent}};
const std::array color_copies {
const std::array narrow_copies {
vk::BufferImageCopy {0,
color.info.pitch,
color.info.extent.height,
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
narrow.info.pitch,
narrow.info.extent.height,
{narrow.info.IsDepth() ? vk::ImageAspectFlagBits::eDepth
: vk::ImageAspectFlagBits::eColor,
0, 0, layers},
{},
color.info.extent}};
narrow.info.extent}};
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
auto depth_buffer = m_tiler.GetScratchBuffer(depth_slice * layers);
auto narrow_buffer = m_tiler.GetScratchBuffer(narrow_slice * layers, depth_buffer.buffer);
const TileManager::D16Layout promote_layout {
.width = depth.info.extent.width,
.height = depth.info.extent.height,
.layers = layers,
.source_row_stride = static_cast<uint64_t>(color.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * transfer_bytes,
.source_slice_stride = color_slice,
.source_row_stride = static_cast<uint64_t>(narrow.info.pitch) * sizeof(uint16_t),
.target_row_stride = static_cast<uint64_t>(depth.info.pitch) * sizeof(uint32_t),
.source_slice_stride = narrow_slice,
.target_slice_stride = depth_slice,
};
const bool d32 = DepthAspectTransferFormat(depth.backing.format) == vk::Format::eD32Sfloat;
if (source_depth) {
if (source_wide) {
source.Download(depth_copies, depth_buffer.buffer, depth_buffer.offset, depth_buffer.size);
m_tiler.ConvertD16(depth_buffer, color_buffer, TileManager::D16Direction::Demote, d32,
m_tiler.ConvertD16(depth_buffer, narrow_buffer, TileManager::D16Direction::Demote, d32,
{.width = promote_layout.width,
.height = promote_layout.height,
.layers = promote_layout.layers,
@@ -550,11 +549,12 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
.target_row_stride = promote_layout.source_row_stride,
.source_slice_stride = promote_layout.target_slice_stride,
.target_slice_stride = promote_layout.source_slice_stride});
destination.Upload(color_copies, color_buffer.buffer, color_buffer.offset,
color_buffer.size);
destination.Upload(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
} else {
source.Download(color_copies, color_buffer.buffer, color_buffer.offset, color_buffer.size);
m_tiler.ConvertD16(color_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
source.Download(narrow_copies, narrow_buffer.buffer, narrow_buffer.offset,
narrow_buffer.size);
m_tiler.ConvertD16(narrow_buffer, depth_buffer, TileManager::D16Direction::Promote, d32,
promote_layout);
destination.Upload(depth_copies, depth_buffer.buffer, depth_buffer.offset,
depth_buffer.size);
@@ -125,6 +125,14 @@ void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdb
}
}
}
if (dirty_state.cull_mode) {
dirty_state.cull_mode = false;
cmdbuf.setCullMode(cull_mode);
}
if (dirty_state.front_face) {
dirty_state.front_face = false;
cmdbuf.setFrontFace(front_face);
}
if (dirty_state.blend_constants) {
dirty_state.blend_constants = false;
cmdbuf.setBlendConstants(blend_constants.data());
@@ -52,6 +52,9 @@ struct DynamicState {
bool stencil_back_write_mask : 1;
bool stencil_back_compare_mask : 1;
bool cull_mode : 1;
bool front_face : 1;
bool blend_constants : 1;
bool color_write_enables : 1;
bool line_width : 1;
@@ -86,6 +89,9 @@ struct DynamicState {
uint32_t stencil_back_write_mask {};
uint32_t stencil_back_compare_mask {};
vk::CullModeFlags cull_mode {};
vk::FrontFace front_face {};
std::array<float, 4> blend_constants {};
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
uint32_t num_color_write_enables {};
@@ -221,6 +227,20 @@ struct DynamicState {
}
}
void SetCullMode(const vk::CullModeFlags cull_mode_) {
if (cull_mode != cull_mode_) {
cull_mode = cull_mode_;
dirty_state.cull_mode = true;
}
}
void SetFrontFace(const vk::FrontFace front_face_) {
if (front_face != front_face_) {
front_face = front_face_;
dirty_state.front_face = true;
}
}
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
if (blend_constants != blend_constants_) {
blend_constants = blend_constants_;
@@ -484,7 +484,7 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
vk::AccessFlagBits::eHostWrite |
vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eShaderWrite;
command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
vk::PipelineStageFlagBits::eComputeShader, {}, 0, nullptr, 2, barriers, 0, nullptr);
@@ -20,7 +20,7 @@ namespace {
namespace IR = ShaderRecompiler::IR;
// Smaller tables are cheap to materialize on each draw and are often transient.
// Smaller tables are often written for one draw, so they enter on a second draw of a frame.
constexpr uint64_t MinTableBytes = 64ull * 1024ull;
constexpr uint32_t ElementMask = (1u << IR::ImageHeapKindShift) - 1u;
@@ -133,12 +133,16 @@ ImageHeap::~ImageHeap() {
bool ImageHeap::Acquire(const IR::ImageHeapRequest& request, IR::ImageHeapView& result) {
KYTY_PROFILER_FUNCTION();
const auto size = request.size & ~uint64_t {3};
if (size < MinTableBytes || request.count == 0u || request.step == 0u ||
if (request.count == 0u || request.step == 0u ||
((request.base | request.first | request.step) & 3u) != 0u) {
return false;
}
auto memory = std::ranges::find_if(
m_memories, [&](const auto& candidate) { return candidate->base == request.base; });
if (memory == m_memories.end() && size < MinTableBytes &&
m_sightings.try_emplace(request.base, m_draw).first->second == m_draw) {
return false;
}
// A table follows its descriptor's size, as the first program of a draw acquires it.
if (memory == m_memories.end() || ((*memory)->size != size && (*memory)->draw != m_draw)) {
if (!m_context.IsMapped(request.base, size)) return false;
@@ -593,26 +597,42 @@ void ImageHeap::UnmapMemory(uint64_t address, uint64_t size) {
} else {
m_unmapped.push_back({address, size});
}
auto& scheduler = m_context.GetCommandScheduler();
std::scoped_lock lock {m_mutex};
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
if (address >= memory->base + memory->size || memory->base >= address + size) {
return false;
}
Unwatch(*memory);
for (auto& view: memory->views) {
std::erase(m_active, view.get());
for (const auto& [value, index]: view->elements) {
Unreference(m_elements[index]);
view->released.push_back(index);
}
Retire(*view);
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
}
Drop(*memory);
return true;
});
}
void ImageHeap::RunGarbageCollector() {
// A smaller table rebuilt each frame moves to a new base. Larger ones keep their dead words.
std::scoped_lock lock {m_mutex};
std::erase_if(m_memories, [&](const std::unique_ptr<Memory>& memory) {
if (memory->size >= MinTableBytes || memory->draw > m_suspend_draw) return false;
Drop(*memory);
return true;
});
m_suspend_draw = m_draw;
m_sightings.clear();
}
void ImageHeap::Drop(Memory& memory) {
auto& scheduler = m_context.GetCommandScheduler();
Unwatch(memory);
for (auto& view: memory.views) {
std::erase(m_active, view.get());
for (const auto& [value, index]: view->elements) {
Unreference(m_elements[index]);
view->released.push_back(index);
}
Retire(*view);
if (scheduler.Active()) {
scheduler.DeferOperation([buffer = std::move(view->buffer)] {});
}
}
}
} // namespace Libs::Graphics
@@ -26,7 +26,7 @@ struct ImageHeapTestAccess;
// of a table owns device slot words, one per table dword, that select an array element. CPU
// writes to a table fault in its snapshot, which each draw refreshes once; a view takes the
// changes when a draw first acquires it. Changes to the images behind elements bump
// Image::heap_epoch.
// Image::heap_epoch. A smaller table leaves at a suspend point if the frame did not acquire it.
class ImageHeap final: public ShaderRecompiler::IR::ImageHeapSource {
public:
ImageHeap(RenderContext& context, PageManager& page_manager);
@@ -47,6 +47,8 @@ public:
// CPU write fault, on any thread.
void InvalidateMemory(uint64_t address, uint64_t size);
void UnmapMemory(uint64_t address, uint64_t size);
// At a suspend point.
void RunGarbageCollector();
private:
using DescriptorValue = ShaderRecompiler::IR::DescriptorValue;
@@ -107,6 +109,7 @@ private:
template <bool track>
void Watch(const Memory& memory, size_t begin, size_t end);
void Unwatch(const Memory& memory);
void Drop(Memory& memory);
void UpdateProbes(View& view, uint64_t begin, uint64_t end);
void UpdateProbe(View& view, uint64_t probe);
void Assign(View& view, size_t slot, uint32_t entry);
@@ -125,9 +128,11 @@ private:
std::mutex m_mutex; // Fault-visible Memory state and m_memories.
std::vector<std::unique_ptr<Memory>> m_memories;
std::vector<View*> m_active; // Acquired since the last commit.
uint64_t m_draw = 1;
uint64_t m_draw = 1;
uint64_t m_suspend_draw = 0; // m_draw at the last suspend point.
std::vector<Element> m_elements;
std::vector<uint32_t> m_free;
std::unordered_map<uint64_t, uint64_t> m_sightings; // Smaller tables: base, first draw.
std::deque<std::pair<uint64_t, uint32_t>> m_retired; // Tick, element.
std::vector<std::pair<size_t, size_t>> m_runs;
std::vector<uint32_t> m_scratch;
@@ -687,6 +687,9 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender
vertex_params[0] = PrepareProgram(vertex_regs, context, user_config, vertex_info[0]);
}
const bool mesh_active = vertex_info[0].logical_stage == ShaderType::Mesh;
if (!tess_active && !mesh_active) {
vertex_info[0].indirect_offsets = state.indirect_offsets;
}
if (object_primitive_id) {
// CxObjectIdMode: register source plus a 32-bit pipelined primitive-ID addition.
EXIT_NOT_IMPLEMENTED(sh.m_paClObjprimIdCntl != 2u || mesh_active || tess_active ||
@@ -1003,15 +1006,11 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline(const CommandBuffer&
}
static_params.alpha_to_coverage_enable = ps_active && ctx.GetShaderRegisters().AlphaToMask();
const bool rect_list = Prospero::IsRectList(command.GetUserConfig().GetPrimType());
static_params.cull_back = !rect_list && mc.cull_back;
static_params.cull_front = !rect_list && mc.cull_front;
static_params.face = mc.face;
static_params.provoking_vtx_last = mc.provoking_vtx_last;
static_params.polygon_mode =
ResolvePolygonMode(mc, static_params.cull_front, static_params.cull_back);
ResolvePolygonMode(mc, !rect_list && mc.cull_front, !rect_list && mc.cull_back);
if (tessellation) {
static_params.patch_control_points =
static_cast<uint8_t>(vs_input_info.tess.input_control_points);
static_params.patch_control_points = vs_input_info.tess.input_control_points;
} else if (!mesh && static_params.topology == vk::PrimitiveTopology::ePatchList) {
// Rectangle lists expand through generated tessellation stages.
static_params.patch_control_points = 3;
@@ -54,15 +54,12 @@ struct PipelineStaticParameters {
bool depth_clip_enable = true;
vk::PrimitiveTopology topology = vk::PrimitiveTopology::ePointList;
// 0 without tessellation, 3 for a rectangle list, else the input control points.
uint8_t patch_control_points = 0;
uint32_t patch_control_points = 0;
bool primitive_restart_enable = false;
uint32_t samples = 1;
bool sample_shading_enable = false;
bool alpha_to_coverage_enable = false;
uint32_t color_mask[RENDER_COLOR_ATTACHMENTS_MAX] = {};
bool cull_front = false;
bool cull_back = false;
bool face = false;
bool provoking_vtx_last = false;
vk::PolygonMode polygon_mode = vk::PolygonMode::eFill;
uint8_t color_srcblend[RENDER_COLOR_ATTACHMENTS_MAX] = {};
@@ -140,6 +137,8 @@ struct DrawRenderState {
bool ps_active = false;
std::array<ShaderVertexInputInfo, 3> vertex_info;
ShaderPixelInputInfo ps_input_info;
// Input: the user-data slots a GPU-read indirect draw patches.
ShaderIndirectOffsets indirect_offsets;
[[nodiscard]] std::span<const ShaderVertexInputInfo> VertexStages() const {
return {vertex_info.data(), vertex_info[1].stage ? 3u : 1u};
@@ -219,17 +219,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::PipelineViewportStateCreateInfo viewport_state {};
viewport_state.pNext = &depth_clip_control;
vk::CullModeFlags cull_mode = vk::CullModeFlagBits::eNone;
if (static_params.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
if (static_params.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
vk::FrontFace front_face =
(static_params.face ? vk::FrontFace::eClockwise : vk::FrontFace::eCounterClockwise);
vk::PipelineRasterizationDepthClipStateCreateInfoEXT clip_ext {};
clip_ext.depthClipEnable = static_params.depth_clip_enable ? VK_TRUE : VK_FALSE;
@@ -250,8 +239,6 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
provoking_vertex.pNext = rasterizer.pNext;
rasterizer.pNext = &provoking_vertex;
}
rasterizer.cullMode = cull_mode;
rasterizer.frontFace = front_face;
rasterizer.polygonMode = static_params.polygon_mode;
rasterizer.lineWidth = 1.0f;
@@ -347,6 +334,8 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p
vk::DynamicState::eViewportWithCount,
vk::DynamicState::eScissorWithCount,
vk::DynamicState::eLineWidth,
vk::DynamicState::eCullMode,
vk::DynamicState::eFrontFace,
vk::DynamicState::eDepthTestEnable,
vk::DynamicState::eDepthWriteEnable,
vk::DynamicState::eDepthCompareOp,
+4 -3
View File
@@ -45,9 +45,10 @@ enum class DrawOffsetSource : uint8_t {
// Nonzero address: the GPU reads count Vulkan indirect draw records, stride bytes apart.
struct DrawIndirectArgs {
uint64_t address = 0;
uint32_t count = 0;
uint32_t stride = 0;
uint64_t address = 0;
uint32_t count = 0;
uint32_t stride = 0;
ShaderIndirectOffsets offsets;
};
struct DrawIndexArgs {
@@ -147,6 +147,7 @@ void RenderContext::RunGarbageCollector() {
m_fault_process_pending = false;
m_buffer_cache.ProcessFaultBuffer();
}
m_image_heap.RunGarbageCollector();
m_texture_cache.ProcessDownloadImages();
m_texture_cache.RunGarbageCollector();
m_buffer_cache.RunGarbageCollector();
+21 -5
View File
@@ -403,6 +403,20 @@ static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
}
// Rect lists are drawn without culling.
vk::CullModeFlags cull_mode {};
if (!Prospero::IsRectList(buffer.GetUserConfig().GetPrimType())) {
if (mode.cull_front) {
cull_mode |= vk::CullModeFlagBits::eFront;
}
if (mode.cull_back) {
cull_mode |= vk::CullModeFlagBits::eBack;
}
}
dynamic_state.SetCullMode(cull_mode);
dynamic_state.SetFrontFace(mode.face ? vk::FrontFace::eClockwise
: vk::FrontFace::eCounterClockwise);
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
if (depth.stencil_test_enable) {
const auto& front = depth.stencil_front;
@@ -1236,8 +1250,9 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
}
}
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need CPU counts, as
// mesh and quad-list draws do. Indirect records carry their own offsets.
// Vulkan offsets for a prepared draw, or nullopt when its indirect records need the CPU: mesh
// and quad-list draws for counts, tessellation draws for patched SGPRs. Indirect records carry
// their own offsets.
static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig& ucfg,
const ShaderVertexInputInfo& vs,
DrawOffsetSource source,
@@ -1246,7 +1261,8 @@ static std::optional<DrawEmitInfo> ResolveDrawEmit(const HW::UserConfig&
const DrawIndirectArgs& indirect) {
if (indirect.address != 0) {
if (vs.stage.module->info.stage == ShaderType::Mesh ||
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy) {
ucfg.GetPrimType() == Prospero::PrimitiveType::kQuadListLegacy ||
vs.indirect_offsets != indirect.offsets) {
return std::nullopt;
}
return DrawEmitInfo {.indirect = indirect};
@@ -1310,7 +1326,7 @@ bool RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
hw_check(buffer);
DrawRenderState state {};
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) {
return true;
}
@@ -1415,7 +1431,7 @@ bool RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
const DrawCallInfo draw {CommandBufferDebugOp::DrawIndexAuto,
args.vertex_count, args.instance_count, args.first_instance};
DrawRenderState state {};
DrawRenderState state {.indirect_offsets = args.indirect.offsets};
if (!GetDrawTopology(ucfg, state.topology)) {
ResetBindings();
return true;
@@ -1,10 +1,11 @@
#version 450
#extension GL_EXT_shader_16bit_storage : require
layout(constant_id = 0) const uint D32 = 0u;
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint16_t data[]; } output_buffer;
layout(push_constant, std430) uniform Conversion {
uint src_base;
@@ -32,9 +33,5 @@ void main() {
}
uint src = params.src_base + y * params.source_pitch + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
uint shift = (dst & 2u) * 8u;
uint mask = 0xffffu << shift;
uint word = dst >> 2u;
atomicAnd(output_buffer.data[word], ~mask);
atomicOr(output_buffer.data[word], decode(input_buffer.data[src >> 2u]) << shift);
output_buffer.data[dst >> 1u] = uint16_t(decode(input_buffer.data[src >> 2u]));
}
@@ -533,16 +533,16 @@ void DrawMemoryAndIo(const Snapshot& shown) {
Bytes(guest.pooled_allocated) + " (" + Bytes(guest.pool_committed) + " committed)");
Row("Flexible", Bytes(guest.flexible_used) + " / " + Bytes(guest.flexible_total));
Row("Extended", Bytes(guest.automatic_allocated));
// Past the pool size the guest sees no page-table entries available.
const auto entries = std::max(guest.cpu_page_entries, guest.gpu_page_entries);
// Past its total a side sees no page-table entries available.
const auto percent = std::max(guest.cpu_page_entries * 100 / guest.cpu_page_entries_total,
guest.gpu_page_entries * 100 / guest.gpu_page_entries_total);
Row("Page table (2 MB)",
Fmt("CPU %llu GPU %llu / %llu",
Fmt("CPU %llu / %llu GPU %llu / %llu",
static_cast<unsigned long long>(guest.cpu_page_entries),
static_cast<unsigned long long>(guest.cpu_page_entries_total),
static_cast<unsigned long long>(guest.gpu_page_entries),
static_cast<unsigned long long>(guest.page_entries_total)),
entries > guest.page_entries_total ? BAD
: entries * 10 > guest.page_entries_total * 9 ? MEDIUM
: GOOD);
static_cast<unsigned long long>(guest.gpu_page_entries_total)),
LevelColor(static_cast<double>(percent), 90, 100));
ImGui::EndTable();
}
if (BeginSection("Emulator process")) {
@@ -51,6 +51,7 @@ struct VulkanExtensions {
vk::PhysicalDeviceVulkan11Features WindowContext::RequiredVulkan11Features() noexcept {
vk::PhysicalDeviceVulkan11Features features {};
features.storageBuffer16BitAccess = VK_TRUE;
features.shaderDrawParameters = VK_TRUE;
return features;
}
@@ -278,6 +279,8 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
required_features12.samplerMirrorClampToEdge);
check_feature(features11.storageBuffer16BitAccess, "storageBuffer16BitAccess",
WindowContext::RequiredVulkan11Features().storageBuffer16BitAccess);
check_feature(features11.shaderDrawParameters, "shaderDrawParameters",
WindowContext::RequiredVulkan11Features().shaderDrawParameters);
check_feature(features12.storageBuffer8BitAccess, "storageBuffer8BitAccess",
required_features12.storageBuffer8BitAccess);
check_feature(features12.timelineSemaphore, "timelineSemaphore",
@@ -63,7 +63,8 @@ uint32_t EmitBuiltinU32(EmitterState& state, IR::StageInputKind kind, uint32_t c
}
if (kind == IR::StageInputKind::VertexIndex || kind == IR::StageInputKind::InstanceIndex ||
kind == IR::StageInputKind::InvocationId || kind == IR::StageInputKind::PrimitiveId ||
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId) {
kind == IR::StageInputKind::Layer || kind == IR::StageInputKind::SampleId ||
kind == IR::StageInputKind::BaseVertex || kind == IR::StageInputKind::BaseInstance) {
const auto value = state.builder.AllocateId();
const auto bits = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, TypeI32(state), value, variable);
@@ -476,6 +476,8 @@ uint32_t BuiltInForInput(IR::StageInputKind kind) {
case IR::StageInputKind::LocalInvocationId: return spv::BuiltInLocalInvocationId;
case IR::StageInputKind::LocalInvocationIndex: return spv::BuiltInLocalInvocationIndex;
case IR::StageInputKind::GlobalInvocationId: return spv::BuiltInGlobalInvocationId;
case IR::StageInputKind::BaseVertex: return spv::BuiltInBaseVertex;
case IR::StageInputKind::BaseInstance: return spv::BuiltInBaseInstance;
default: return UINT32_MAX;
}
}
@@ -526,7 +528,9 @@ void DefineInputs(EmitterState& state) {
case IR::StageInputKind::PrimitiveId:
case IR::StageInputKind::InstanceIndex:
case IR::StageInputKind::Layer:
case IR::StageInputKind::SampleId: type = TypeI32(state); break;
case IR::StageInputKind::SampleId:
case IR::StageInputKind::BaseVertex:
case IR::StageInputKind::BaseInstance: type = TypeI32(state); break;
case IR::StageInputKind::WorkgroupId:
case IR::StageInputKind::NumWorkgroups:
case IR::StageInputKind::LocalInvocationId:
@@ -802,6 +806,10 @@ void DefineModule(EmitterState& state) {
if (InputVariableForKind(state, IR::StageInputKind::SampleId) != 0) {
state.builder.RequireCapability(spv::CapabilitySampleRateShading);
}
if (InputVariableForKind(state, IR::StageInputKind::BaseVertex) != 0 ||
InputVariableForKind(state, IR::StageInputKind::BaseInstance) != 0) {
state.builder.RequireCapability(spv::CapabilityDrawParameters);
}
if (state.program.info.image_gather_extended) {
state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
}
@@ -1389,10 +1389,27 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
// Vulkan adds an indirect record's bases to the indices; the hardware passes them
// only in the user SGPRs the packet patches.
const auto& offsets = options.input_info.vertex->indirect_offsets;
const auto index = [&](uint8_t slot, IR::StageInputKind kind,
IR::StageInputKind base_kind) {
if (offsets == ShaderIndirectOffsets {}) {
return builtin(kind);
}
const auto base = builtin(base_kind);
if (slot < options.user_data_count) {
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(options.user_data_base + slot),
base);
}
return entry_ir.ISub(builtin(kind), base);
};
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex));
index(offsets.vertex, IR::StageInputKind::VertexIndex,
IR::StageInputKind::BaseVertex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
builtin(IR::StageInputKind::InstanceIndex));
index(offsets.instance, IR::StageInputKind::InstanceIndex,
IR::StageInputKind::BaseInstance));
}
}
const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
@@ -118,15 +118,14 @@ ResourcePlan& ResourcePlan::operator=(ResourcePlan&& other) noexcept {
}
Program::~Program() {
// Planning expressions can refer to block values but outlive block storage in the base class.
// Planning expressions can refer to block values but outlive block storage in the base class,
// and values cross block boundaries: detach everything before any storage is destroyed.
for (auto& inst: value_storage) {
inst.Invalidate();
inst.DropArgs();
}
// Values may cross block boundaries. Detach all arguments before any block starts destroying
// its instruction storage so reverse-use links always point to live definitions.
for (auto* block: blocks) {
for (auto& inst: *block) {
inst.Invalidate();
inst.DropArgs();
}
}
}
@@ -251,6 +251,8 @@ enum class StageInputKind {
LocalInvocationId,
LocalInvocationIndex,
GlobalInvocationId,
BaseVertex,
BaseInstance,
Parameter,
};
+15 -71
View File
@@ -25,22 +25,6 @@ Value Value::F32(float value) {
return Value(Type::F32, std::bit_cast<uint32_t>(value));
}
bool Value::IsEmpty() const {
return type == Type::Void;
}
bool Value::IsImmediate() const {
return type != Type::Opaque;
}
bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
Type Value::GetType() const {
if (IsPhi()) {
return inst->Flags<Type>();
@@ -56,19 +40,6 @@ Inst* Value::Instruction() const {
return inst;
}
Inst* Value::TryInstruction() const {
return type == Type::Opaque ? inst : nullptr;
}
Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
ScalarReg Value::ScalarRegister() const {
EXIT_IF(type != Type::ScalarReg);
return scalar_reg;
@@ -94,11 +65,6 @@ uint16_t Value::U16() const {
return imm_u16;
}
uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
uint64_t Value::U64() const {
EXIT_IF(type != Type::U64);
return imm_u64;
@@ -152,10 +118,6 @@ Inst::~Inst() {
ClearArgs();
}
ValueOpcode Inst::GetOpcode() const {
return opcode;
}
Type Inst::GetType() const {
if (opcode == ValueOpcode::Phi) {
return static_cast<Type>(flags);
@@ -178,51 +140,25 @@ size_t Inst::UseCount() const {
return uses.size();
}
size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
size_t Inst::NumPhiBlocks() const {
return num_args == PhiArity ? phi_args.size() : 0;
}
Value Inst::Arg(size_t index) const {
EXIT_IF(index >= NumArgs());
if (num_args <= InlineArity) {
return fixed_args[index];
}
return num_args == PhiArity ? phi_args[index].second : large_args[index];
}
Block* Inst::PhiBlock(size_t index) const {
EXIT_IF(opcode != ValueOpcode::Phi || index >= phi_args.size());
return phi_args[index].first;
}
Block* Inst::Parent() const {
return parent;
}
const std::vector<Use>& Inst::Uses() const {
return uses;
}
void Inst::SetParent(Block* block) {
parent = block;
}
void Inst::SetArg(size_t index, Value value) {
const auto old = Arg(index);
if (auto* old_inst = old.TryInstruction(); old_inst != nullptr) {
auto& arg = ArgSlot(*this, index);
if (auto* old_inst = arg.TryInstruction(); old_inst != nullptr) {
RemoveUse(old_inst, index);
}
if (num_args <= InlineArity) {
fixed_args[index] = value;
} else if (num_args == PhiArity) {
phi_args[index].second = value;
} else {
large_args[index] = value;
}
arg = value;
if (auto* new_inst = value.TryInstruction(); new_inst != nullptr) {
AddUse(new_inst, index);
}
@@ -238,11 +174,15 @@ void Inst::AddPhiOperand(Block* predecessor, Value value) {
}
void Inst::ReplaceUsesWith(Value replacement, bool preserve) {
const auto old_uses = uses;
for (const auto& use: old_uses) {
use.user->SetArg(use.operand, replacement);
}
// Detach the arguments first (a phi can use itself), then move every use over in order.
Invalidate();
const auto old_uses = std::exchange(uses, {});
for (const auto& use: old_uses) {
ArgSlot(*use.user, use.operand) = replacement;
}
if (auto* target = replacement.TryInstruction(); target != nullptr) {
target->uses.insert(target->uses.end(), old_uses.begin(), old_uses.end());
}
if (preserve) {
opcode = ValueOpcode::Identity;
num_args = 1;
@@ -272,6 +212,10 @@ void Inst::ClearArgs() {
RemoveUse(value_inst, index);
}
}
DropArgs();
}
void Inst::DropArgs() {
if (num_args == PhiArity) {
std::destroy_at(&phi_args);
std::construct_at(&fixed_args);
+50 -6
View File
@@ -33,14 +33,14 @@ public:
static Value F16(uint16_t bits);
static Value F32(float value);
[[nodiscard]] bool IsEmpty() const;
[[nodiscard]] bool IsImmediate() const;
[[nodiscard]] bool IsEmpty() const { return type == Type::Void; }
[[nodiscard]] bool IsImmediate() const { return type != Type::Opaque; }
[[nodiscard]] bool IsIdentity() const;
[[nodiscard]] bool IsPhi() const;
[[nodiscard]] Type GetType() const;
[[nodiscard]] Inst* Instruction() const;
[[nodiscard]] Inst* TryInstruction() const;
[[nodiscard]] Inst* TryInstruction() const { return type == Type::Opaque ? inst : nullptr; }
[[nodiscard]] Inst* ResolveInstruction() const;
[[nodiscard]] Value Resolve() const;
[[nodiscard]] ScalarReg ScalarRegister() const;
@@ -112,7 +112,7 @@ public:
Inst(Inst&&) = delete;
Inst& operator=(Inst&&) = delete;
[[nodiscard]] ValueOpcode GetOpcode() const;
[[nodiscard]] ValueOpcode GetOpcode() const { return opcode; }
[[nodiscard]] Type GetType() const;
[[nodiscard]] bool MayHaveSideEffects() const;
[[nodiscard]] bool HasUses() const;
@@ -121,8 +121,8 @@ public:
[[nodiscard]] size_t NumPhiBlocks() const;
[[nodiscard]] Value Arg(size_t index) const;
[[nodiscard]] Block* PhiBlock(size_t index) const;
[[nodiscard]] Block* Parent() const;
[[nodiscard]] const std::vector<Use>& Uses() const;
[[nodiscard]] Block* Parent() const { return parent; }
[[nodiscard]] const std::vector<Use>& Uses() const { return uses; }
// SRT walker output index, assigned by the resource plan that owns this instruction.
[[nodiscard]] uint32_t WalkOutput() const { return walk_output; }
void SetWalkOutput(uint32_t index) const { walk_output = index; }
@@ -138,6 +138,8 @@ public:
void AddPhiOperand(Block* predecessor, Value value);
void ReplaceUsesWith(Value replacement, bool preserve = true);
void Invalidate();
// Drops the arguments without updating their use lists, for teardown that destroys both sides.
void DropArgs();
template <typename T>
requires(sizeof(T) <= sizeof(uint64_t) && std::is_trivially_copyable_v<T>)
@@ -157,6 +159,8 @@ public:
private:
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
template <typename Self>
static auto& ArgSlot(Self& self, size_t index);
void AddUse(Inst* used, size_t operand);
void RemoveUse(Inst* used, size_t operand);
void ClearArgs();
@@ -181,4 +185,44 @@ private:
static_assert(sizeof(Inst) <= 120, "Inst operand and backend storage unintentionally increased");
// Called per operand by the IR passes and the resource walker; inline, as the build has no LTO.
inline bool Value::IsIdentity() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Identity;
}
inline bool Value::IsPhi() const {
return type == Type::Opaque && inst->GetOpcode() == ValueOpcode::Phi;
}
inline Inst* Value::ResolveInstruction() const {
EXIT_IF(type != Type::Opaque);
return IsIdentity() ? inst->Arg(0).ResolveInstruction() : inst;
}
inline Value Value::Resolve() const {
return IsIdentity() ? inst->Arg(0).Resolve() : *this;
}
inline uint32_t Value::U32() const {
EXIT_IF(type != Type::U32);
return imm_u32;
}
inline size_t Inst::NumArgs() const {
return num_args == PhiArity ? phi_args.size() : num_args;
}
template <typename Self>
auto& Inst::ArgSlot(Self& self, size_t index) {
EXIT_IF(index >= self.NumArgs());
if (self.num_args <= InlineArity) {
return self.fixed_args[index];
}
return self.num_args == PhiArity ? self.phi_args[index].second : self.large_args[index];
}
inline Value Inst::Arg(size_t index) const {
return ArgSlot(*this, index);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -175,6 +175,8 @@ void AddBuiltinInput(ShaderInfo& info, StageInputKind kind) {
case StageInputKind::GlobalInvocationId:
AddInput(info, kind, 0, 3, "gl_GlobalInvocationID");
break;
case StageInputKind::BaseVertex: AddInput(info, kind, 0, 1, "gl_BaseVertex"); break;
case StageInputKind::BaseInstance: AddInput(info, kind, 0, 1, "gl_BaseInstance"); break;
case StageInputKind::PackedAncillary:
case StageInputKind::Parameter: break;
}
@@ -444,6 +446,8 @@ void Visit(Program& program, ShaderStageInputInfo input_info, InputUsage& inputs
case StageInputKind::InstanceIndex:
case StageInputKind::FrontFacing:
case StageInputKind::LocalInvocationIndex:
case StageInputKind::BaseVertex:
case StageInputKind::BaseInstance:
if (component != 0u) {
return Fail("typed scalar builtin component is out of range");
}
+1
View File
@@ -422,6 +422,7 @@ void BuildStageStaticKey(const ShaderVertexInputInfo& info, std::vector<uint32_t
key.push_back(info.lds_size_dwords);
key.push_back(info.pa_cl_vs_out_cntl);
key.push_back(info.user_clip_planes);
key.push_back(info.indirect_offsets.vertex | (uint32_t {info.indirect_offsets.instance} << 8u));
key.push_back(static_cast<uint32_t>(info.clip_space.enabled));
if (info.clip_space.enabled) {
for (const float value: info.clip_space.scale) {
+13
View File
@@ -124,6 +124,18 @@ struct ShaderTessellationInputInfo {
uint32_t output_topology = 0;
};
// GS user-data slots an indirect draw packet writes its record's base vertex and first instance
// to. Like the hardware index VGPRs, the vertex shader's indices exclude both bases once either
// slot is set.
struct ShaderIndirectOffsets {
static constexpr uint8_t None = UINT8_MAX;
uint8_t vertex = None;
uint8_t instance = None;
bool operator==(const ShaderIndirectOffsets&) const = default;
};
struct ShaderVertexInputInfo {
static constexpr int RES_MAX = 32;
@@ -146,6 +158,7 @@ struct ShaderVertexInputInfo {
ShaderClipSpaceTransform clip_space;
ShaderMeshInputInfo mesh;
ShaderTessellationInputInfo tess;
ShaderIndirectOffsets indirect_offsets;
bool fetch_external = false;
bool fetch_embedded = false;
};
+40 -29
View File
@@ -9,6 +9,7 @@
#include "graphics/host_gpu/renderer/renderContext.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/systemContent.h"
#include <algorithm>
#include <array>
@@ -63,15 +64,21 @@ constexpr int PROT_GPU_WRITE = 0x20;
enum class GpuAccessMode { NoAccess, Read, Write, ReadWrite };
constexpr uint64_t PAGE_TABLE_POOL_SIZE = 4ull * 1024ull * 1024ull * 1024ull;
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr int PAGE_TABLE_POOL_ENTRIES =
static_cast<int>(PAGE_TABLE_POOL_SIZE / PAGE_TABLE_GRANULARITY);
constexpr uint64_t PAGE_TABLE_GRANULARITY = 2ull * 1024ull * 1024ull;
constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 448ull * 1024ull * 1024ull;
// A game gets 12.5 GiB. A fixed 64 MiB cache and mapping table memory past the 64 MiB included
// for each of the CPU, GPU and AMM tables come off the top; flexible and direct memory share what
// is left. One 4 KiB table maps 2 MiB.
constexpr uint64_t GUEST_MEMORY_BUDGET = 12800ull * 1024ull * 1024ull;
constexpr uint64_t RESERVED_CACHE_SIZE = 64ull * 1024ull * 1024ull;
constexpr uint64_t INCLUDED_TABLE_MEMORY = 64ull * 1024ull * 1024ull;
constexpr uint64_t MAPPING_TABLE_SIZE = 4ull * 1024ull;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
static Graphics::RenderContext* g_gpu_resources = nullptr;
static uint64_t g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE;
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static uint64_t g_cpu_table_memory = INCLUDED_TABLE_MEMORY;
static uint64_t g_gpu_table_memory = INCLUDED_TABLE_MEMORY;
static Graphics::RenderContext* g_gpu_resources = nullptr;
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
#endif
@@ -664,11 +671,8 @@ public:
KYTY_CLASS_NO_COPY(PhysicalMemory);
static constexpr uint64_t TotalSize() { return static_cast<uint64_t>(13824) * 1024 * 1024; }
static uint64_t Size() {
EXIT_IF(g_flexible_memory_size >= TotalSize());
return TotalSize() - g_flexible_memory_size;
}
static uint64_t TotalSize() { return g_guest_memory_size; }
static uint64_t Size() { return TotalSize() - g_flexible_memory_size; }
bool Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type,
@@ -998,7 +1002,22 @@ static bool SelfTestSub64SharedPlaceholderAlias() {
static bool ReplaceFixedRangeWithReserved(uint64_t start, uint64_t size);
void Initialize() {
g_flexible_memory_size_frozen = true;
const auto params = Loader::SystemContentGetMemoryParams();
if (params.flexible_size != 0) {
g_flexible_memory_size = params.flexible_size;
}
g_cpu_table_memory = std::max(params.cpu_table_memory, INCLUDED_TABLE_MEMORY);
g_gpu_table_memory = std::max(params.gpu_table_memory, INCLUDED_TABLE_MEMORY);
const auto table_overhead = g_cpu_table_memory + g_gpu_table_memory +
std::max(params.amm_table_memory, INCLUDED_TABLE_MEMORY) -
3 * INCLUDED_TABLE_MEMORY;
EXIT_IF(g_flexible_memory_size + table_overhead >= GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE);
g_guest_memory_size = GUEST_MEMORY_BUDGET - RESERVED_CACHE_SIZE - table_overhead;
LOGF("\t flexible memory size = %" PRIu64 " MiB, direct memory size = %" PRIu64 " MiB\n",
g_flexible_memory_size / (1024ull * 1024ull),
(g_guest_memory_size - g_flexible_memory_size) / (1024ull * 1024ull));
VirtualMemory::Init();
g_guest_address_space = std::make_unique<GuestAddressSpace>(PhysicalMemory::TotalSize());
g_physical_memory = std::make_unique<PhysicalMemory>();
@@ -1066,15 +1085,6 @@ void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func) {
}
}
void SetFlexibleMemorySize(uint64_t size) {
constexpr uint64_t GuestPageSize = 0x4000;
EXIT_IF(g_flexible_memory_size_frozen || g_guest_address_space != nullptr);
EXIT_IF(size == 0 || (size & (GuestPageSize - 1u)) != 0 || size >= PhysicalMemory::TotalSize());
g_flexible_memory_size = size;
LOGF("\t flexible memory size = 0x%016" PRIx64 " (%" PRIu64 " MiB)\n", size,
size / (1024ull * 1024ull));
}
bool PhysicalMemory::Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type, AllocationKind kind) {
if (phys_addr_out == nullptr) {
@@ -2655,12 +2665,12 @@ int KYTY_SYSV_ABI KernelGetPageTableStats(int* cpu_total, int* cpu_available, in
const auto gpu_used =
(g_virtual_ranges != nullptr ? g_virtual_ranges->CountPageTableEntries(true) : 0);
*cpu_total = PAGE_TABLE_POOL_ENTRIES;
*gpu_total = PAGE_TABLE_POOL_ENTRIES;
*cpu_available = PAGE_TABLE_POOL_ENTRIES -
static_cast<int>(std::min<uint64_t>(cpu_used, PAGE_TABLE_POOL_ENTRIES));
*gpu_available = PAGE_TABLE_POOL_ENTRIES -
static_cast<int>(std::min<uint64_t>(gpu_used, PAGE_TABLE_POOL_ENTRIES));
const auto cpu_tables = g_cpu_table_memory / MAPPING_TABLE_SIZE;
const auto gpu_tables = g_gpu_table_memory / MAPPING_TABLE_SIZE;
*cpu_total = static_cast<int>(cpu_tables);
*gpu_total = static_cast<int>(gpu_tables);
*cpu_available = static_cast<int>(cpu_tables - std::min(cpu_used, cpu_tables));
*gpu_available = static_cast<int>(gpu_tables - std::min(gpu_used, gpu_tables));
LOGF_COLOR(Log::Color::Green,
"\t cpu_total = %d\n"
@@ -4381,7 +4391,8 @@ DebugStats GetDebugStats() {
stats.direct_total = PhysicalMemory::Size();
stats.flexible_total = FlexibleMemory::Size();
stats.pool_committed = g_memory_pool_committed.load(std::memory_order_relaxed);
stats.page_entries_total = PAGE_TABLE_POOL_ENTRIES;
stats.cpu_page_entries_total = g_cpu_table_memory / MAPPING_TABLE_SIZE;
stats.gpu_page_entries_total = g_gpu_table_memory / MAPPING_TABLE_SIZE;
if (g_physical_memory != nullptr) {
Common::LockGuard lock(g_physical_memory->GetMutex());
for (const auto& [start, block]: g_physical_memory->GetPhysicalBlocks()) {
+4 -4
View File
@@ -26,9 +26,10 @@ struct DebugStats {
uint64_t flexible_total = 0;
uint64_t flexible_used = 0;
uint64_t pool_committed = 0;
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
uint64_t gpu_page_entries = 0;
uint64_t page_entries_total = 0; // Per side.
uint64_t cpu_page_entries = 0; // 2 MiB page-table entries in use.
uint64_t gpu_page_entries = 0;
uint64_t cpu_page_entries_total = 0;
uint64_t gpu_page_entries_total = 0;
};
[[nodiscard]] DebugStats GetDebugStats();
@@ -125,7 +126,6 @@ static_assert(sizeof(KernelMemoryPoolBlockStats) == 16,
"KernelMemoryPoolBlockStats struct size is incorrect");
void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
void SetFlexibleMemorySize(uint64_t size);
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
int memory_type, int64_t* phys_addr_out, bool automatic = false);
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
+1
View File
@@ -125,6 +125,7 @@ if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
install(FILES ${KYTY_NOTO_SANS_FONTS} DESTINATION "KytyPS5.app/Contents/Resources/assets/fonts")
install(DIRECTORY "${CMAKE_SOURCE_DIR}/assets/cursors" DESTINATION "KytyPS5.app/Contents/Resources/assets")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
+20 -46
View File
@@ -2,10 +2,12 @@
#include "common/assert.h"
#include "common/common.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "loader/symbolDatabase.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cinttypes>
@@ -675,19 +677,6 @@ static bool font_is_italic(const FontState* font) {
font->render_effect_slant != 0.0f;
}
static const char* fallback_font_file_name(bool bold, bool italic) {
if (bold && italic) {
return "Roboto-BoldItalic.ttf";
}
if (bold) {
return "Roboto-Bold.ttf";
}
if (italic) {
return "Roboto-Italic.ttf";
}
return "Roboto-Regular.ttf";
}
static std::vector<uint8_t> read_file_bytes(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file) {
@@ -709,49 +698,34 @@ static std::vector<uint8_t> read_file_bytes(const std::filesystem::path& path) {
return data;
}
static std::vector<std::filesystem::path> fallback_font_candidates(const char* file_name) {
const auto rel =
std::filesystem::path("3rdparty") / "tracy" / "profiler" / "src" / "font" / file_name;
std::vector<std::filesystem::path> paths;
auto source_root = std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
paths.push_back(source_root / rel);
std::error_code ec;
auto cwd = std::filesystem::current_path(ec);
if (!ec) {
paths.push_back(cwd / rel);
paths.push_back(cwd / ".." / rel);
paths.push_back(cwd / ".." / ".." / rel);
static std::vector<uint8_t> load_fallback_font_data(const char* file_name) {
const char* base_path = SDL_GetBasePath();
if (base_path == nullptr) {
LOGF("Font: SDL_GetBasePath failed: %s\n", SDL_GetError());
return {};
}
return paths;
}
static std::vector<uint8_t> load_fallback_font_data(bool bold, bool italic) {
for (const auto& path: fallback_font_candidates(fallback_font_file_name(bold, italic))) {
auto data = read_file_bytes(path);
if (!data.empty()) {
return data;
}
// SDL returns UTF-8; path(const char*) would read it in the Windows ANSI code page.
const auto path = Common::PathFromUtf8(base_path) / "assets" / "fonts" / file_name;
auto data = read_file_bytes(path);
if (data.empty()) {
LOGF("Font: fallback font not found: %s\n", Common::PathToString(path).c_str());
}
return {};
return data;
}
static const std::vector<uint8_t>& fallback_font_data(bool bold, bool italic) {
static const auto regular = load_fallback_font_data(false, false);
static const auto bold_data = load_fallback_font_data(true, false);
static const auto italic_data = load_fallback_font_data(false, true);
static const auto bold_italic = load_fallback_font_data(true, true);
static const auto regular = load_fallback_font_data("NotoSans-Regular.ttf");
static const auto bold_data = load_fallback_font_data("NotoSans-Bold.ttf");
static const auto italic_data = load_fallback_font_data("NotoSans-Italic.ttf");
static const auto bold_italic = load_fallback_font_data("NotoSans-BoldItalic.ttf");
if (bold && italic && !bold_italic.empty()) {
if (bold && italic) {
return bold_italic;
}
if (bold && !bold_data.empty()) {
if (bold) {
return bold_data;
}
if (italic && !italic_data.empty()) {
if (italic) {
return italic_data;
}
return regular;
+21 -11
View File
@@ -35,7 +35,7 @@ public:
bool GetParamInt(const char* name, int32_t* value);
bool GetParamString(const char* name, std::string* value);
bool GetParamString(const char* name, char* value, size_t value_size);
uint64_t GetFlexibleMemorySize();
MemoryParams GetMemoryParams();
void DbgPrint();
@@ -61,7 +61,7 @@ private:
Common::Mutex m_mutex;
bool m_opened = false;
std::vector<Param> m_params;
uint64_t m_flexible_memory_size = 0;
MemoryParams m_memory_params;
};
class PlayGo {
@@ -172,11 +172,21 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
add_int_param("userDefinedParam3", "USER_DEFINED_PARAM_3");
add_int_param("userDefinedParam4", "USER_DEFINED_PARAM_4");
auto read_size = [](const nlohmann::ordered_json& object, const char* key, uint64_t* size,
uint64_t unit = 1) {
if (auto it = object.find(key); it != object.end()) {
*size = it->get<uint64_t>() * unit;
}
};
if (auto kernel = param_json.find("kernel");
kernel != param_json.end() && kernel->is_object()) {
if (auto it = kernel->find("flexibleMemorySize"); it != kernel->end()) {
m_flexible_memory_size = it->get<uint64_t>();
}
read_size(*kernel, "flexibleMemorySize", &m_memory_params.flexible_size);
read_size(*kernel, "cpuPageTableSize", &m_memory_params.cpu_table_memory);
read_size(*kernel, "gpuPageTableSize", &m_memory_params.gpu_table_memory);
}
if (auto amm = param_json.find("amm"); amm != param_json.end() && amm->is_object()) {
read_size(*amm, "pagetableMemorySizeInMib", &m_memory_params.amm_table_memory,
1024ull * 1024ull);
}
auto localized = param_json.find("localizedParameters");
@@ -227,8 +237,8 @@ void Psf::Close() {
Common::LockGuard lock(m_mutex);
m_params.clear();
m_flexible_memory_size = 0;
m_opened = false;
m_memory_params = {};
m_opened = false;
}
bool Psf::IsValid() {
@@ -339,10 +349,10 @@ bool Psf::GetParamString(const char* name, char* value, size_t value_size) {
return false;
}
uint64_t Psf::GetFlexibleMemorySize() {
MemoryParams Psf::GetMemoryParams() {
Common::LockGuard lock(m_mutex);
return m_flexible_memory_size;
return m_memory_params;
}
PlayGo::~PlayGo() {
@@ -646,10 +656,10 @@ bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_
return sc->psf.GetParamString(name, value, value_size);
}
uint64_t SystemContentGetFlexibleMemorySize() {
MemoryParams SystemContentGetMemoryParams() {
auto* sc = Common::Singleton<SystemContent>::Instance();
return sc->psf.GetFlexibleMemorySize();
return sc->psf.GetMemoryParams();
}
bool SystemContentGetIconPath(std::filesystem::path* path) {
+10 -1
View File
@@ -14,7 +14,16 @@ void SystemContentLoadParamSfo(const std::filesystem::path& file_name);
bool SystemContentParamSfoGetInt(const char* name, int32_t* value);
bool SystemContentParamSfoGetString(const char* name, std::string* value);
bool SystemContentParamSfoGetString(const char* name, char* value, size_t value_size);
uint64_t SystemContentGetFlexibleMemorySize();
// Memory sizes a game's param.json sets, in bytes; 0 where it sets none.
struct MemoryParams {
uint64_t flexible_size = 0;
uint64_t cpu_table_memory = 0;
uint64_t gpu_table_memory = 0;
uint64_t amm_table_memory = 0;
};
MemoryParams SystemContentGetMemoryParams();
bool SystemContentGetIconPath(std::filesystem::path* path);
bool SystemContentGetChunksNum(uint32_t* num);
+4 -1
View File
@@ -65,6 +65,8 @@ else()
SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
endif()
# Guest code runs natively and needs x86-64-v3 (AVX2, BMI2, FMA), so every host has it.
# No FMA contraction: a * b + c keeps both roundings.
if(KYTY_CLANG_CL)
if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]")
string(REGEX REPLACE "/W[0-4]" "/W3" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
@@ -79,7 +81,7 @@ if(KYTY_CLANG_CL)
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE}")
string(REGEX REPLACE "/MD" "/MT" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /wd4244 /wd4305 /wd4800 /wd4345")
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /utf-8 /Oy- /clang:-march=x86-64-v3 /clang:-ffp-contract=off /wd4244 /wd4305 /wd4800 /wd4345")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} /WX")
@@ -103,6 +105,7 @@ elseif(CLANG OR GCC)
else()
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -fno-exceptions -fdiagnostics-color=always -finput-charset=UTF-8 -fexec-charset=UTF-8 -static-libgcc -static-libstdc++ -g -fno-strict-aliasing -fno-omit-frame-pointer -Wall -Wno-unused-value -fmessage-length=0")
endif()
set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -march=x86-64-v3 -ffp-contract=off")
if(KYTY_WARNINGS_ARE_ERRORS)
#set(KYTY_CPP_FLAGS "${KYTY_CPP_FLAGS} -Werror")
+17
View File
@@ -0,0 +1,17 @@
#include <cstdio>
#if !defined(__AVX2__) || !defined(__BMI2__) || !defined(__FMA__)
#error "x86-64 builds target x86-64-v3"
#endif
int main() {
// (1 + 2^-23)^2 = 1 + 2^-22 + 2^-46; only a fused multiply-add keeps the 2^-46.
volatile float a = 1.0f + 0x1p-23f;
const float result = a * a - (1.0f + 0x1p-22f);
if (result != 0.0f) {
std::fprintf(stderr, "BuildFlagsTests: failed: a * a - b was contracted (%a)\n", result);
return 1;
}
std::puts("BuildFlagsTests: all cases passed");
return 0;
}
+30
View File
@@ -353,6 +353,35 @@ void TestOperandMutationLifecycle() {
"Phi replacement retained incoming uses or changed the consumer value");
}
void TestReplacementKeepsUseOrder() {
Fixture fixture(2);
const auto base = fixture.Emit(ValueOpcode::IAdd32, {Value(1u), Value(2u)});
auto *before = fixture.Emit(ValueOpcode::IAdd32, {base, Value(3u)}).TryInstruction();
auto *identity = fixture.Emit(ValueOpcode::Identity, {base}).TryInstruction();
auto *pair = fixture.Emit(ValueOpcode::IAdd32, {Value(identity), Value(identity)}).TryInstruction();
auto *after = fixture.Emit(ValueOpcode::IAdd32, {Value(4u), base}).TryInstruction();
auto &large = fixture.program.value_storage.emplace_back(ValueOpcode::GetImageResource);
const auto last = large.NumArgs() - 1;
large.SetArg(last, Value(identity));
identity->ReplaceUsesWith(base, false);
Check(!identity->HasUses() && identity->NumArgs() == 0 && pair->Arg(0) == base &&
pair->Arg(1) == base && large.Arg(last) == base &&
base.TryInstruction()->Uses() ==
std::vector<Use>({{before, 0}, {after, 1}, {pair, 0}, {pair, 1}, {&large, last}}),
"identity removal lost or reordered the uses of its argument");
auto &phi = fixture.BlockAt(1).AppendNewInst(ValueOpcode::Phi);
phi.SetFlags(Type::U32);
phi.AddPhiOperand(&fixture.BlockAt(0), Value(after));
phi.AddPhiOperand(&fixture.BlockAt(1), Value(&phi));
auto *user = fixture.Emit(ValueOpcode::IAdd32, {Value(&phi), Value(&phi)}, 0, 1).TryInstruction();
phi.ReplaceUsesWith(Value(after));
Check(phi.GetOpcode() == ValueOpcode::Identity && phi.Arg(0) == Value(after) && !phi.HasUses() &&
user->Arg(0) == Value(after) && user->Arg(1) == Value(after) &&
after->Uses() == std::vector<Use>({{user, 0}, {user, 1}, {&phi, 0}}),
"self-referencing Phi replacement kept a use of itself or reordered the moved uses");
}
void TestControlDependentStandaloneLoadStaysTyped() {
Fixture fixture(3);
const auto memory = fixture.AddMemory(ResourceKind::ScalarAddress);
@@ -916,6 +945,7 @@ int main() {
TestCarryAndBitFields();
TestInvariantAndDivergentPhi();
TestOperandMutationLifecycle();
TestReplacementKeepsUseOrder();
TestControlDependentStandaloneLoadStaysTyped();
TestRuntime64BitDescriptorOps();
TestUniformFirstLaneSamplerLod();
+190 -29
View File
@@ -3942,6 +3942,62 @@ public:
"write invalidation without a remaining copy did not establish CPU "
"ownership");
// A RELEASE_MEM label beside GPU-written bytes lands without downloading
// them; one over GPU-written bytes downloads them first, and one on a clean
// cached page leaves it CPU-modified for the next upload.
constexpr uint64_t label_gpu_address = fault_base + 0xf000;
constexpr uint64_t label_beside_address = fault_base + 0xf100;
constexpr uint64_t label_clean_address = memory_dst + 0x100;
constexpr uint32_t label_gpu_value = 0x7e57ab1eu;
constexpr uint64_t label_value = 0x0123456789abcdefull;
gpu.SendCommandSync([&] {
auto dirty = buffer_cache.ObtainBuffer(
label_gpu_address, sizeof(label_gpu_value), true, false);
Require("GpuCommandLane", "label page setup",
dirty.first != nullptr &&
buffer_cache.IsRegionRegistered(label_clean_address,
sizeof(label_value)) &&
!buffer_cache.IsRegionCpuModified(label_clean_address,
sizeof(label_value)) &&
!buffer_cache.IsRegionGpuModified(label_clean_address,
sizeof(label_value)),
"label test has no GPU-written page or clean cached page");
buffer_cache.FillBuffer(label_gpu_address, sizeof(label_gpu_value),
label_gpu_value, false);
});
const auto release_label = [&](uint64_t address) {
const auto packet = make_interrupt_packet(
2, 0, reinterpret_cast<void *>(address), label_value, 0);
gpu.Submit(packet);
gpu.WaitForIdle();
uint64_t label = 0;
return LibKernel::Memory::TryReadBacking(address, &label,
sizeof(label)) &&
label == label_value;
};
LibKernel::Memory::InstallGpuResources(&resources);
const bool label_beside_stored =
release_label(label_beside_address) &&
buffer_cache.HasGpuDirtyBytes(label_gpu_address,
sizeof(label_gpu_value));
const bool label_over_stored =
release_label(label_gpu_address) &&
!buffer_cache.HasGpuDirtyBytes(label_gpu_address, sizeof(label_value));
const bool label_clean_stored =
release_label(label_clean_address) &&
buffer_cache.IsRegionCpuModified(label_clean_address,
sizeof(label_value));
LibKernel::Memory::InstallGpuResources(nullptr);
Require("GpuCommandLane", "RELEASE_MEM label beside GPU-written bytes",
label_beside_stored,
"a label downloaded the GPU-written bytes beside it");
Require("GpuCommandLane", "RELEASE_MEM label over GPU-written bytes",
label_over_stored,
"a label over GPU-written bytes left them to a later download");
Require("GpuCommandLane", "RELEASE_MEM label on a clean cached page",
label_clean_stored,
"a label left a clean cached copy of its page stale");
resources.UnmapMemory(fault_base, fault_size);
Require("GpuCommandLane", "processor fault unmap",
Libs::LibKernel::Memory::KernelMunmap(fault_base, fault_size) == 0,
@@ -5628,6 +5684,25 @@ public:
ImageHeapTestAccess::MemoryCount(heap) == 0u &&
textures.GetImage(resident).heap_refs.load() == 0u,
"unmapping the table kept its images pinned");
// A smaller table enters once a second draw acquires it and leaves at the suspend point
// after a frame that did not acquire it. Larger tables stay.
const IR::ImageHeapRequest small{table, table_size / 2u, 32u, 0u, table_size / 64u, &sampled};
Require(name, "first small draw",
acquire(sampled, idle_table) && !heap.Acquire(small, view) &&
!heap.Acquire(small, view),
"a smaller table entered on its first draw");
draw();
Require(name, "second small draw", heap.Acquire(small, view),
"a smaller table was declined on its second draw");
context.RunGarbageCollector();
Require(name, "acquired frame", ImageHeapTestAccess::MemoryCount(heap) == 2u,
"a table left at the suspend point of a frame that acquired it");
draw();
context.RunGarbageCollector();
Require(name, "idle frame",
ImageHeapTestAccess::MemoryCount(heap) == 1u &&
textures.GetImage(resident).heap_refs.load() == 0u,
"a suspend point kept an idle smaller table pinned or dropped a larger one");
// The idle table stays watched; destroying the heap must unwatch it before the page
// manager fails on live watches.
Require(name, "idle table",
@@ -9903,37 +9978,78 @@ public:
d16_storage_desc.view_info.usage = vk::ImageUsageFlagBits::eStorage;
const auto d16_storage_image =
texture_cache.FindImage(d16_storage_desc, true);
auto d16_storage_readback = CreateHostBuffer(
name, sizeof(d16_fallback_values),
vk::BufferUsageFlagBits::eTransferDst, std::vector<u32>(2, 0));
auto &d16_storage_native = texture_cache.GetImage(d16_storage_image);
d16_storage_native.Transit(vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead, {},
scheduler.Current().Handle());
vk::BufferImageCopy d16_storage_copy{};
d16_storage_copy.bufferRowLength = 4;
d16_storage_copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0,
0, 1};
d16_storage_copy.imageExtent = {4, 1, 1};
scheduler.Current().Handle().copyImageToBuffer(
d16_storage_native.backing.image,
vk::ImageLayout::eTransferSrcOptimal, d16_storage_readback.buffer, 1,
&d16_storage_copy);
HostReadBarrier(d16_storage_readback.buffer, d16_storage_readback.size,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
const auto d16_storage_words = ReadBuffer(name, d16_storage_readback, 2);
std::array<uint16_t, 4> d16_storage_values{};
std::memcpy(d16_storage_values.data(), d16_storage_words.data(),
sizeof(d16_storage_values));
const auto read_texels = [&](ImageId id, vk::ImageAspectFlagBits aspect) {
auto readback = CreateHostBuffer(name, 4 * sizeof(u32),
vk::BufferUsageFlagBits::eTransferDst,
std::vector<u32>(4, 0));
auto &image = texture_cache.GetImage(id);
image.Transit(vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead, {},
scheduler.Current().Handle());
const vk::BufferImageCopy copy{0, 4, 0, {aspect, 0, 0, 1}, {}, {4, 1, 1}};
scheduler.Current().Handle().copyImageToBuffer(
image.backing.image, vk::ImageLayout::eTransferSrcOptimal,
readback.buffer, 1, &copy);
HostReadBarrier(readback.buffer, readback.size,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
auto words = ReadBuffer(name, readback, 4);
DestroyBuffer(&readback);
return words;
};
const auto d16_storage_words =
read_texels(d16_storage_image, vk::ImageAspectFlagBits::eColor);
Require(
name, "D16 fallback depth/storage copy",
d16_storage_values == d16_fallback_values &&
std::memcmp(d16_storage_words.data(), d16_fallback_values.data(),
sizeof(d16_fallback_values)) == 0 &&
d16_storage_image != d16_depth_image &&
texture_cache.GetImage(d16_storage_image).IsGpuModified(),
"D16 fallback reinterpreted four-byte host depth as two-byte color");
DestroyBuffer(&d16_storage_readback);
// Dropping and re-adding the stencil plane keeps the D16 depth values.
constexpr uint64_t d16_stenciled_offset = 0x33e000;
constexpr std::array<uint16_t, 4> d16_stale_values{};
std::memcpy(memory + d16_stenciled_offset, d16_fallback_values.data(),
sizeof(d16_fallback_values));
auto d16_stenciled_desc = d16_depth_desc;
d16_stenciled_desc.info.data.address = base + d16_stenciled_offset;
d16_stenciled_desc.info.stencil.address = base + 0x33f000;
const auto d16_stenciled_image =
texture_cache.FindImage(d16_stenciled_desc);
(void)texture_cache.FindDepthTarget(d16_stenciled_image,
d16_stenciled_desc);
texture_cache.MarkGpuWritten(d16_stenciled_image);
// Stale guest bytes: the values below can come only from image copies.
Libs::LibKernel::Memory::WriteBacking(base + d16_stenciled_offset,
d16_stale_values.data(),
sizeof(d16_stale_values));
auto d16_native_desc = d16_stenciled_desc;
d16_native_desc.info.stencil = {};
d16_native_desc.info.pixel_format = vk::Format::eD16Unorm;
d16_native_desc.view_info.format = vk::Format::eD16Unorm;
d16_native_desc.view_info.aspect = vk::ImageAspectFlagBits::eDepth;
const auto d16_native_image = texture_cache.FindImage(d16_native_desc);
const auto d16_native_words =
read_texels(d16_native_image, vk::ImageAspectFlagBits::eDepth);
Require(name, "D16 fallback depth/native depth copy",
std::memcmp(d16_native_words.data(), d16_fallback_values.data(),
sizeof(d16_fallback_values)) == 0 &&
texture_cache.GetImage(d16_native_image).backing.format ==
vk::Format::eD16Unorm,
"D16 depth lost its values when its target dropped the stencil");
const auto d16_restenciled_image =
texture_cache.FindImage(d16_stenciled_desc);
const auto d16_restenciled_words =
read_texels(d16_restenciled_image, vk::ImageAspectFlagBits::eDepth);
bool d16_promoted = d16_restenciled_image != d16_native_image;
for (size_t i = 0; i < d16_fallback_values.size(); ++i) {
d16_promoted &= (d16_restenciled_words[i] & 0x00ffffffu) ==
EncodeD16AsD24(d16_fallback_values[i]);
}
Require(name, "D16 native depth/fallback depth copy", d16_promoted,
"D16 depth lost its values when its target regained the stencil");
if (!texture_cache.GetImage(combined_destination_image).IsGpuModified()) {
texture_cache.MarkGpuWritten(combined_destination_image);
@@ -18770,6 +18886,10 @@ public:
cmd.setViewportWithCount(1, &viewport);
cmd.setScissorWithCount(1, &scissor);
cmd.setLineWidth(1);
cmd.setCullMode(vk::CullModeFlagBits::eNone);
cmd.setFrontFace(registers.GetModeControl().face
? vk::FrontFace::eClockwise
: vk::FrontFace::eCounterClockwise);
cmd.setDepthTestEnable(depth.depth_test_enable);
cmd.setDepthWriteEnable(depth.depth_write_enable);
cmd.setDepthCompareOp(depth.depth_compare_op);
@@ -18967,6 +19087,9 @@ public:
const std::array stencil_pipelines{
pipeline(true, 2, 2, false, true).pipeline,
pipeline(true, 2, 2, false, false).pipeline};
Require(name, "dynamic winding pipeline reuse",
stencil_pipelines[0] == stencil_pipelines[1],
"the winding changed the graphics pipeline");
const auto check_stencil = [&](const char *label, bool back_face,
uint8_t initial, uint8_t written) {
vk::ClearValue clear{};
@@ -19599,6 +19722,22 @@ public:
"native vertex or instance offset was applied twice");
}
}
// With back faces culled, the triangle draws for exactly one winding.
const auto draws_with_back_cull = [&](bool clockwise) {
registers.SetModeControl({.cull_back = true, .face = clockwise});
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
RenderExecutorTestAccess::DrawAuto(executor, scheduler.Current(),
{.vertex_count = 3, .instance_count = 1, .first_vertex = 7, .first_instance = 5,
.offset_source = DrawOffsetSource::IndirectArgs});
const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id,
{}, {extent, extent, 1});
return std::ranges::any_of(pixels, [](u32 word) { return word != 0; });
};
Require("DynamicCullMode", "back faces culled by winding",
draws_with_back_cull(false) != draws_with_back_cull(true),
"the draw's cull mode or front face did not reach the host GPU");
registers.SetModeControl({});
// Indirect records come from the GPU copy; the CPU backing holds zeros. In each pair the
// first record draws nothing and the second covers the target, through vertex offset 7
@@ -19665,11 +19804,12 @@ public:
for (u32 i = 0; i < 4; i++) {
shaders.SetGsUserSgpr(i, static_cast<u32>(tables[i / 2] >> (i % 2 * 32)));
}
const auto check = [&](const char *step, uint64_t vertex_shader, bool indexed, u32 first) {
const auto check = [&](const char *step, uint64_t vertex_shader, bool indexed, u32 first,
ShaderIndirectOffsets offsets = {}) {
shaders.SetEsShaderBase(vertex_shader);
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
const DrawIndirectArgs records{args_address + first * stride, 2, stride};
const DrawIndirectArgs records{args_address + first * stride, 2, stride, offsets};
const bool drawn = indexed
? RenderExecutorTestAccess::DrawIndex(executor, scheduler.Current(),
{.index_count = 3, .index_addr = reinterpret_cast<const void *>(index_address),
@@ -19695,6 +19835,16 @@ public:
"no cached graphics pipeline named only compiled modules");
check("recreated indexed embedded fetch", fetch_vs, true, 0);
check("recreated auto native fetch", vertex_address, false, 6);
// Patched user SGPRs 0/1 read the records' base vertex 7 and first instance 5, not the
// table pointers they hold, and the index VGPRs exclude both bases.
check("indexed patched offsets", indirect_vs, true, 0, {0, 1});
check("auto patched offsets", indirect_vs, false, 4, {0, 1});
// Slots 4/5 lie past USER_SGPR, so the tables survive; rewritten fetches keep Vulkan's
// vertex offset.
check("indexed patched embedded fetch", fetch_vs, true, 0, {4, 5});
// Patching one offset still leaves both bases out of the index VGPRs.
check("auto instance-only patch", vertex_address, false, 5,
{ShaderIndirectOffsets::None, 1});
}
// A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner.
@@ -19744,9 +19894,10 @@ public:
registers.SetPsInputEna(2);
registers.SetPsInputAddr(2);
registers.SetPsInputSettings(0, 0);
// Rect lists are drawn without culling.
const auto check_rect = [&](Prospero::PrimitiveType primitive, const char *rect_name) {
user_config.SetPrimitiveType(primitive);
registers.SetModeControl({});
registers.SetModeControl({.cull_front = true, .cull_back = true});
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {});
RenderExecutorTestAccess::DrawAuto(
@@ -51429,6 +51580,16 @@ void CheckPm4DrawIndirectMultiPacket(RenderContext &renderer) {
auto_packet[3] = Pm4::SH_NOP;
arguments[3] = 103;
check_offsets(auto_packet, hs, {53, 101}, "disabled destinations");
// GS destinations, with an index buffer size for indexed draws, leave the records to the
// GPU and the CP writes no SGPR. A first-index destination stays on the CPU.
processor.SetIndexBufferSize(3);
arguments = {0, 1, 61, 17, 19};
check_offsets(indexed_packet, gs, {41, 7, 8}, "GPU indexed records");
auto_packet[2] = Pm4::SPI_SHADER_USER_DATA_GS_0 + 9;
auto_packet[3] = Pm4::SPI_SHADER_USER_DATA_GS_0 + 10;
check_offsets(auto_packet, gs, {41, 7, 8}, "GPU records");
indexed_packet[3] |= 1u << 28u;
check_offsets(indexed_packet, gs, {17, 19, 61}, "first-index destination");
processor.BufferFlush();
std::printf("[host] %-32s ok\n", "Pm4DrawIndirectMulti");
}
+21 -8
View File
@@ -88,9 +88,14 @@ using Libs::LibKernel::Memory::VirtualQueryInfo;
// Prospero ABI?
constexpr uint64_t SceKernelPageSize = 0x4000;
constexpr uint64_t SceKernelTotalPhysicalSize = 13824ull * 1024ull * 1024ull;
constexpr uint64_t SceKernelFlexibleMemorySize = 448ull * 1024ull * 1024ull;
constexpr uint64_t TestFlexibleMemorySize = 3072ull * 1024ull * 1024ull;
constexpr uint64_t GuestMemoryBudget = 12800ull * 1024ull * 1024ull;
constexpr uint64_t ReservedCacheSize = 64ull * 1024ull * 1024ull;
// The 128 MiB CPU and 96 MiB AMM mapping tables add 64 + 32 MiB; a zero GPU table size adds none.
constexpr uint64_t TestCpuTableMemory = 128ull * 1024ull * 1024ull;
constexpr uint64_t TestAmmTableMemory = 96ull * 1024ull * 1024ull;
constexpr uint64_t TestTableOverhead = 96ull * 1024ull * 1024ull;
constexpr int SceKernelProtCpuRead = 0x01;
constexpr int SceKernelProtCpuRw = 0x02;
constexpr int SceKernelProtCpuExec = 0x04;
@@ -175,7 +180,9 @@ void InitSubsystems() {
const auto param_json = std::filesystem::temp_directory_path() /
("kyty_virtual_memory_" +
std::to_string(reinterpret_cast<uintptr_t>(&initialized)) + ".json");
constexpr char json[] = R"({"kernel":{"flexibleMemorySize":3221225472}})";
constexpr char json[] =
R"({"kernel":{"flexibleMemorySize":3221225472,"cpuPageTableSize":134217728,)"
R"("gpuPageTableSize":0},"amm":{"pagetableMemorySizeInMib":96}})";
Common::File param_file;
Check("InitSubsystems", param_file.Create(param_json), "failed to create temporary param.json");
uint32_t bytes_written = 0;
@@ -185,16 +192,19 @@ void InitSubsystems() {
"failed to write temporary param.json");
Loader::SystemContentLoadParamSfo(param_json);
const auto flexible_memory_size = Loader::SystemContentGetFlexibleMemorySize();
const auto params = Loader::SystemContentGetMemoryParams();
Check("InitSubsystems", Common::File::DeleteFile(param_json),
"failed to remove temporary param.json");
Check("InitSubsystems", flexible_memory_size == TestFlexibleMemorySize,
Check("InitSubsystems", params.flexible_size == TestFlexibleMemorySize,
"failed to read flexible memory size from param.json");
Check("InitSubsystems",
params.cpu_table_memory == TestCpuTableMemory && params.gpu_table_memory == 0 &&
params.amm_table_memory == TestAmmTableMemory,
"failed to read mapping table sizes from param.json");
size_t default_flexible_memory_size = 0;
Libs::LibKernel::Memory::KernelConfiguredFlexibleMemorySize(&default_flexible_memory_size);
Check("InitSubsystems", default_flexible_memory_size == SceKernelFlexibleMemorySize,
"flexible memory does not default to 448 MiB without param.json");
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
subsystems.Initialize<Libs::LibKernel::Memory::Lifecycle>();
@@ -822,8 +832,8 @@ void TestFlexibleMemoryCapacityIsBootFixed() {
Check(test, configured == TestFlexibleMemorySize,
"boot flexible pool did not use the param.json value");
Check(test, configured == baseline, "boot flexible pool did not start at configured capacity");
Check(test, backing == SceKernelTotalPhysicalSize,
"boot backing is not the single 13.5 GiB physical file");
Check(test, backing == GuestMemoryBudget - ReservedCacheSize - TestTableOverhead,
"boot backing is not the 12.5 GiB budget less the reserved cache and table overhead");
Check(test, backing == Libs::LibKernel::Memory::KernelGetDirectMemorySize() + configured,
"direct and flexible regions do not partition the boot backing");
@@ -1409,7 +1419,10 @@ void TestPageTableStatsTrackRangeEdits() {
"KernelGetPageTableStats");
return stats;
};
const auto baseline = read_stats();
// A 4 KiB table maps 2 MiB: 128 MiB of CPU tables gives 32768, the default 64 MiB GPU 16384.
const auto baseline = read_stats();
Check(test, baseline[0] == 32768 && baseline[2] == 16384,
"page-table totals do not follow the configured table memory");
const auto expect_used = [&](int cpu, int gpu, const char* action) {
const auto stats = read_stats();
Check(test,