diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..970ed7f62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -284,6 +284,7 @@ struct VulkanDevice::State { bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; bool storageImageWithoutFormat = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -1150,6 +1151,21 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_KHR_physical_storage_buffer"); state->depthRangeUnrestricted = hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); if (state->depthRangeUnrestricted) deviceExtensions.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME)) { + VkPhysicalDeviceSampleLocationsPropertiesEXT locations{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &locations}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { + const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); + for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + if ((locations.sampleLocationSampleCounts & samples) == 0) continue; + VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; + multisampleProperties(selected, samples, &multisample); + if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; + } + } + if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -3019,6 +3035,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; return context; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..0fa6d5c8b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -170,6 +170,7 @@ struct Context { bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; template TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..0db0b87eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -106,6 +106,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +void ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index bb18791e6..80959a49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -116,10 +116,13 @@ struct State { VkPipelineColorBlendAttachmentState blend; std::array blendConstants; std::uint32_t samples = 1; + std::array sampleLocations{}; + bool customSampleLocations = false; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..56005f036 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,6 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + if ((context.sampleLocationSampleCounts & target.samples) != 0) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b1fe81c22..a957824c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,11 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: } mix(state.renderExtent.width); mix(state.renderExtent.height); + mix(state.samples); + mix(state.customSampleLocations); + if (state.customSampleLocations) { + for (const auto location : state.sampleLocations) mix(location); + } if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); return key; } @@ -1065,6 +1070,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleLocations(context, state); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 27bfd7ee9..ad1caed7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -106,6 +106,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } +void ValidateSampleLocations(const Context& context, const State& state) { + Require(!state.customSampleLocations || (context.sampleLocationSampleCounts & state.samples) != 0, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -206,6 +210,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + ValidateSampleLocations(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -367,6 +372,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = static_cast(state.samples); + const auto locations = SampleLocations(state); + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; + if ((context.sampleLocationSampleCounts & state.samples) != 0) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -563,6 +573,16 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V begin.renderPass = renderPass; begin.framebuffer = framebuffer.Handle(); begin.renderArea = {{0, 0}, extent}; + const auto locations = SampleLocations(state); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), {1, 1}, state.samples, locations.data()}; + const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; + sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if ((context.sampleLocationSampleCounts & state.samples) != 0) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } @@ -687,6 +707,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); + append(key, state.customSampleLocations); + if (state.customSampleLocations) append(key, state.sampleLocations); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3316fd778..6a089940e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -327,7 +327,12 @@ std::uint32_t decodeSamples(const Registers& cx) { } Require(log2 <= 3u, "more than eight samples are unsupported"); Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); - const auto samples = 1u << log2; + return 1u << log2; +} + +void decodeSampleLocations(const Registers& cx, State& state) { + const auto samples = state.samples; + if (samples == 1) return; const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { for (std::uint32_t sample = 0; sample < samples; ++sample) { @@ -335,10 +340,14 @@ std::uint32_t decodeSamples(const Registers& cx) { const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; const auto x = static_cast(static_cast(bits << 4u)) >> 4; const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; - Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + if (pixel == 0) { + state.sampleLocations[sample] = static_cast(bits); + state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; + } else { + Require(bits == state.sampleLocations[sample], "nonstandard sample locations varying between pixels are unsupported"); + } } } - return samples; } bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { @@ -519,6 +528,23 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } +std::array SampleLocations(const State& state) { + Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); + std::array locations{}; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::array position{}; + if (state.customSampleLocations) { + const auto bits = state.sampleLocations[sample]; + position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + position = {standard[0], standard[1]}; + } + locations[sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; + } + return locations; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -619,6 +645,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); result.samples = decodeSamples(cx); + decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0103b5aec..21c8ab46d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1752,6 +1752,41 @@ void multisampleTests() { } state = DecodeState(queue); Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue.context[0x2f8] = 0x00200002; + queue.context[0x31d] = 0x12000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); + AgcDriver::Graphics::Context sampleContext{}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); + sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); + sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + state = DecodeState(queue); + Require(!state.customSampleLocations, "standard sample locations require the extension"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto samples : {2u, 8u}) { + queue = multisampled(); + const auto log2 = samples == 2 ? 1u : 3u; + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0x77887788; + queue.context[0x2ff + pixel * 4u] = 0x77887788; + } + state = DecodeState(queue); + Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); + const auto endpoints = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; + Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); + } + } queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..8a389c755 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -555,6 +555,7 @@ Throughout the project, every function at every stage either **does exactly what - [Multisampled CMASK state](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled targets in CB passes](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [Custom MSAA sample locations](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid throw. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws.