mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
8757 lines
435 KiB
Diff
8757 lines
435 KiB
Diff
diff --git a/include/indium/device.hpp b/include/indium/device.hpp
|
|
index 1572e13..5ebcb5b 100644
|
|
--- a/include/indium/device.hpp
|
|
+++ b/include/indium/device.hpp
|
|
@@ -51,6 +51,8 @@ namespace Indium {
|
|
*/
|
|
virtual void pollEvents(uint64_t timeoutNanoseconds) = 0;
|
|
virtual void wakeupEventLoop() = 0;
|
|
+ virtual bool supportsDepth24Stencil8PixelFormat() = 0;
|
|
+
|
|
};
|
|
|
|
std::shared_ptr<Device> createSystemDefaultDevice();
|
|
diff --git a/include/indium/library.hpp b/include/indium/library.hpp
|
|
index 70f2b28..2c2e579 100644
|
|
--- a/include/indium/library.hpp
|
|
+++ b/include/indium/library.hpp
|
|
@@ -1,6 +1,8 @@
|
|
#pragma once
|
|
|
|
#include <indium/base.hpp>
|
|
+#include <indium/types.hpp>
|
|
+#include <iridium/iridium.hpp>
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
@@ -17,6 +19,11 @@ namespace Indium {
|
|
virtual std::shared_ptr<Device> device() = 0;
|
|
|
|
INDIUM_PROPERTY_READONLY(std::string, n, N,ame);
|
|
+
|
|
+ public:
|
|
+ // Shader argument metadata, used to build Metal pipeline reflection
|
|
+ virtual const std::vector<Iridium::ArgumentInfo>& argumentInfo() const = 0;
|
|
+ virtual FunctionType functionType() const = 0;
|
|
};
|
|
|
|
class Library {
|
|
diff --git a/include/indium/render-command-encoder.hpp b/include/indium/render-command-encoder.hpp
|
|
index d17993c..0458265 100644
|
|
--- a/include/indium/render-command-encoder.hpp
|
|
+++ b/include/indium/render-command-encoder.hpp
|
|
@@ -19,7 +19,16 @@ namespace Indium {
|
|
public:
|
|
virtual ~RenderCommandEncoder() = 0;
|
|
|
|
- virtual void setRenderPipelineState(std::shared_ptr<RenderPipelineState> renderPipelineState) = 0;
|
|
+ // (darling-lol:) The resource parameters are passed by const reference rather than by
|
|
+ // value: a by-value std::shared_ptr parameter makes the *caller* copy the pointer (an
|
|
+ // atomic increment plus the temporary's increment/decrement pair) and hands the callee a
|
|
+ // private address to copy out of. Every implementation here copies the pointer once into
|
|
+ // the table or op record that has to keep the resource alive anyway, so the reference
|
|
+ // removes one atomic round trip and one store-to-load chain per forwarded bind. These
|
|
+ // signatures are part of the Metal<->Indium pair (both are built together and installed
|
|
+ // together, see patches/metal.patch and patches/metal--deps--indium.patch), and no other
|
|
+ // runtime module calls a render encoder.
|
|
+ virtual void setRenderPipelineState(const std::shared_ptr<RenderPipelineState>& renderPipelineState) = 0;
|
|
virtual void setFrontFacingWinding(Winding frontFaceWinding) = 0;
|
|
virtual void setCullMode(CullMode cullMode) = 0;
|
|
virtual void setDepthBias(float depthBias, float slopeScale, float clamp) = 0;
|
|
@@ -31,7 +40,7 @@ namespace Indium {
|
|
virtual void setScissorRects(const ScissorRect* scissorRects, size_t count) = 0;
|
|
virtual void setScissorRects(const std::vector<ScissorRect>& scissorRects) = 0;
|
|
virtual void setBlendColor(float red, float green, float blue, float alpha) = 0;
|
|
- virtual void setDepthStencilState(std::shared_ptr<DepthStencilState> state) = 0;
|
|
+ virtual void setDepthStencilState(const std::shared_ptr<DepthStencilState>& state) = 0;
|
|
virtual void setTriangleFillMode(TriangleFillMode triangleFillMode) = 0;
|
|
virtual void setStencilReferenceValue(uint32_t value) = 0;
|
|
virtual void setStencilReferenceValue(uint32_t front, uint32_t back) = 0;
|
|
@@ -41,35 +50,37 @@ namespace Indium {
|
|
virtual void drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount) = 0;
|
|
virtual void drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount) = 0;
|
|
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) = 0;
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount) = 0;
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset) = 0;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) = 0;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount) = 0;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset) = 0;
|
|
|
|
virtual void setVertexBytes(const void* bytes, size_t length, size_t index) = 0;
|
|
- virtual void setVertexBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) = 0;
|
|
+ virtual void setVertexBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) = 0;
|
|
virtual void setVertexBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) = 0;
|
|
virtual void setVertexBufferOffset(size_t offset, size_t index) = 0;
|
|
- virtual void setVertexSamplerState(std::shared_ptr<SamplerState> state, size_t index) = 0;
|
|
- virtual void setVertexSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) = 0;
|
|
+ virtual void setVertexSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) = 0;
|
|
+ virtual void setVertexSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) = 0;
|
|
virtual void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) = 0;
|
|
virtual void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, const std::vector<float>& lodMinClamps, const std::vector<float>& lodMaxClamps, Range<size_t> range) = 0;
|
|
- virtual void setVertexTexture(std::shared_ptr<Texture> texture, size_t index) = 0;
|
|
+ virtual void setVertexTexture(const std::shared_ptr<Texture>& texture, size_t index) = 0;
|
|
virtual void setVertexTextures(const std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) = 0;
|
|
|
|
virtual void setFragmentBytes(const void* bytes, size_t length, size_t index) = 0;
|
|
- virtual void setFragmentBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) = 0;
|
|
+ virtual void setFragmentBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) = 0;
|
|
virtual void setFragmentBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) = 0;
|
|
virtual void setFragmentBufferOffset(size_t offset, size_t index) = 0;
|
|
- virtual void setFragmentSamplerState(std::shared_ptr<SamplerState> state, size_t index) = 0;
|
|
- virtual void setFragmentSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) = 0;
|
|
+ virtual void setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) = 0;
|
|
+ virtual void setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) = 0;
|
|
virtual void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) = 0;
|
|
virtual void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, const std::vector<float>& lodMinClamps, const std::vector<float>& lodMaxClamps, Range<size_t> range) = 0;
|
|
- virtual void setFragmentTexture(std::shared_ptr<Texture> texture, size_t index) = 0;
|
|
+ virtual void setFragmentTexture(const std::shared_ptr<Texture>& texture, size_t index) = 0;
|
|
virtual void setFragmentTextures(std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) = 0;
|
|
|
|
virtual void useResource(std::shared_ptr<Resource> resource, ResourceUsage usage, RenderStages stages) = 0;
|
|
virtual void useResources(const std::vector<std::shared_ptr<Resource>>& resources, ResourceUsage usage, RenderStages stages) = 0;
|
|
virtual void useResource(std::shared_ptr<Resource> resource, ResourceUsage usage) = 0;
|
|
virtual void useResources(const std::vector<std::shared_ptr<Resource>>& resources, ResourceUsage usage) = 0;
|
|
+ virtual void memoryBarrier(size_t scope, RenderStages after, RenderStages before) = 0;
|
|
+
|
|
};
|
|
};
|
|
diff --git a/include/iridium/iridium.hpp b/include/iridium/iridium.hpp
|
|
index 900d03a..ae5eff1 100644
|
|
--- a/include/iridium/iridium.hpp
|
|
+++ b/include/iridium/iridium.hpp
|
|
@@ -1,6 +1,7 @@
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
+#include <cstdint>
|
|
|
|
#include <vector>
|
|
#include <unordered_map>
|
|
@@ -90,9 +91,25 @@ namespace Iridium {
|
|
float lodMax;
|
|
};
|
|
|
|
+
|
|
+ struct StructMemberInfo {
|
|
+ size_t offset;
|
|
+ std::string typeName;
|
|
+ std::string name;
|
|
+ };
|
|
+ struct ArgumentInfo {
|
|
+ BindingType type;
|
|
+ size_t index = 0;
|
|
+ std::string name;
|
|
+ std::string typeName;
|
|
+ size_t size = 0;
|
|
+ size_t alignment = 0;
|
|
+ std::vector<StructMemberInfo> members;
|
|
+ };
|
|
struct FunctionInfo {
|
|
FunctionType type;
|
|
std::vector<BindingInfo> bindings;
|
|
+ std::vector<ArgumentInfo> arguments;
|
|
std::vector<EmbeddedSampler> embeddedSamplers;
|
|
};
|
|
|
|
diff --git a/private-include/indium/blit-command-encoder.private.hpp b/private-include/indium/blit-command-encoder.private.hpp
|
|
index d6e71b2..ae31d39 100644
|
|
--- a/private-include/indium/blit-command-encoder.private.hpp
|
|
+++ b/private-include/indium/blit-command-encoder.private.hpp
|
|
@@ -19,21 +19,28 @@ namespace Indium {
|
|
std::shared_ptr<PrivateDevice> _privateDevice;
|
|
BlitPassDescriptor _descriptor;
|
|
|
|
- void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size);
|
|
+ // destination textures that need presentation synchronization (drawables)
|
|
+ std::vector<std::shared_ptr<Texture>> _readWriteTextures;
|
|
+
|
|
+ void noteDestination(const std::shared_ptr<Texture>& destination);
|
|
+
|
|
+ void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size);
|
|
|
|
public:
|
|
PrivateBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer);
|
|
PrivateBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer, const BlitPassDescriptor& descriptor);
|
|
~PrivateBlitCommandEncoder();
|
|
|
|
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) override;
|
|
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override;
|
|
- virtual void copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) override;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) override;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) override;
|
|
- virtual void fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) override;
|
|
- virtual void generateMipmapsForTexture(std::shared_ptr<Texture> texture) override;
|
|
+ const std::vector<std::shared_ptr<Texture>>& readWriteTextures() const { return _readWriteTextures; };
|
|
+
|
|
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) override;
|
|
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) override;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) override;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) override;
|
|
+ virtual void fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) override;
|
|
+ virtual void generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) override;
|
|
|
|
virtual void endEncoding() override;
|
|
};
|
|
diff --git a/private-include/indium/buffer.private.hpp b/private-include/indium/buffer.private.hpp
|
|
index a348b2b..9ad8975 100644
|
|
--- a/private-include/indium/buffer.private.hpp
|
|
+++ b/private-include/indium/buffer.private.hpp
|
|
@@ -29,5 +29,10 @@ namespace Indium {
|
|
|
|
INDIUM_PROPERTY(VkBuffer, b, B,uffer) = VK_NULL_HANDLE;
|
|
INDIUM_PROPERTY(VkDeviceMemory, m, M,emory) = VK_NULL_HANDLE;
|
|
+
|
|
+ private:
|
|
+ // appended last: the device address of a buffer never changes, and it was asked from the driver for every buffer
|
|
+ // binding of every draw
|
|
+ uint64_t _gpuAddress = 0;
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/command-buffer.private.hpp b/private-include/indium/command-buffer.private.hpp
|
|
index b7ffcce..5b1e84c 100644
|
|
--- a/private-include/indium/command-buffer.private.hpp
|
|
+++ b/private-include/indium/command-buffer.private.hpp
|
|
@@ -2,16 +2,22 @@
|
|
|
|
#include <vulkan/vulkan.h>
|
|
|
|
+#include <indium/blit-command-encoder.hpp>
|
|
#include <indium/command-buffer.hpp>
|
|
#include <indium/command-encoder.hpp>
|
|
+#include <indium/compute-command-encoder.hpp>
|
|
|
|
#include <vector>
|
|
#include <mutex>
|
|
#include <condition_variable>
|
|
+#include <atomic>
|
|
|
|
namespace Indium {
|
|
class PrivateCommandQueue;
|
|
class PrivateDevice;
|
|
+ class PrivateRenderCommandEncoder;
|
|
+ class PrivateBlitCommandEncoder;
|
|
+ class PrivateComputeCommandEncoder;
|
|
|
|
class PrivateCommandBuffer: public CommandBuffer, public std::enable_shared_from_this<PrivateCommandBuffer> {
|
|
private:
|
|
@@ -25,7 +31,6 @@ namespace Indium {
|
|
bool _committed = false;
|
|
std::condition_variable _completedCondvar;
|
|
bool _completed = false;
|
|
-
|
|
public:
|
|
PrivateCommandBuffer(std::shared_ptr<PrivateCommandQueue> commandQueue);
|
|
~PrivateCommandBuffer();
|
|
@@ -46,6 +51,25 @@ namespace Indium {
|
|
virtual std::shared_ptr<CommandQueue> commandQueue() override;
|
|
virtual std::shared_ptr<Device> device() override;
|
|
|
|
+ // creates the real render encoder immediately (what renderCommandEncoder() did before deferred encoding existed)
|
|
+ std::shared_ptr<PrivateRenderCommandEncoder> renderCommandEncoderNow(const RenderPassDescriptor& descriptor);
|
|
+ // same for blit/compute: the real encoder is created on the worker that replays the deferred op list
|
|
+ std::shared_ptr<PrivateBlitCommandEncoder> blitCommandEncoderNow();
|
|
+ std::shared_ptr<PrivateBlitCommandEncoder> blitCommandEncoderNow(const BlitPassDescriptor& descriptor);
|
|
+ std::shared_ptr<PrivateComputeCommandEncoder> computeCommandEncoderNow(const ComputePassDescriptor& descriptor);
|
|
+ // leases the Vulkan command buffer (pool, allocation, begin) on the calling thread if that has not happened yet;
|
|
+ // only used with /tmp/metal-worker-lease (by default the constructor leases it, as in round 3)
|
|
+ void ensureRecordingStarted();
|
|
+ void leaseRecording();
|
|
+ void deferredBegin();
|
|
+ void deferredEnd();
|
|
+ // blocks until every deferred encoder of this command buffer has been replayed
|
|
+ void waitDeferred();
|
|
+ // false while the commit has not started. A deferred replay may only be posted while the Vulkan command buffer
|
|
+ // is still open; a facade dropped after the commit (a Metal contract violation) then logs instead of recording
|
|
+ // into a closed command buffer.
|
|
+ bool replayAllowed() const { return !_committedAtomic.load(std::memory_order_acquire); }
|
|
+
|
|
void addScheduledHandlerLocked(std::function<void(std::shared_ptr<CommandBuffer>)> handler);
|
|
void addCompletedHandlerLocked(std::function<void(std::shared_ptr<CommandBuffer>)> handler);
|
|
|
|
@@ -53,5 +77,15 @@ namespace Indium {
|
|
INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice);
|
|
|
|
INDIUM_PROPERTY(VkCommandBuffer, c, C,ommandBuffer) = VK_NULL_HANDLE;
|
|
+ private:
|
|
+ VkCommandPool _recordingPool = VK_NULL_HANDLE;
|
|
+ // Appended LAST on purpose: QuartzCore and Metal are compiled against the earlier layout of this class and read
|
|
+ // commandBuffer() and friends by offset, so no existing member may move.
|
|
+ // deferred (asynchronously replayed) render encoders that have ended but are not applied to _commandBuffer yet
|
|
+ unsigned _deferredPending = 0;
|
|
+ std::mutex _deferredMutex;
|
|
+ std::condition_variable _deferredCondvar;
|
|
+ // set at the start of the commit body (see replayAllowed()); appended last, like the members above
|
|
+ std::atomic<bool> _committedAtomic {false};
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/command-encoder.private.hpp b/private-include/indium/command-encoder.private.hpp
|
|
index c3b1b93..3dab2ce 100644
|
|
--- a/private-include/indium/command-encoder.private.hpp
|
|
+++ b/private-include/indium/command-encoder.private.hpp
|
|
@@ -1,6 +1,7 @@
|
|
#pragma once
|
|
|
|
#include <vector>
|
|
+#include <algorithm>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <array>
|
|
@@ -17,117 +18,295 @@
|
|
|
|
#include <iridium/iridium.hpp>
|
|
|
|
+#include <cstring>
|
|
+#include <cstdio>
|
|
+#include <atomic>
|
|
+
|
|
namespace Indium {
|
|
+ bool bindingReuseEnabled();
|
|
+ // Metal's tracked resources carry writes from one encoder into the next.
|
|
+ // Queue submission order alone supplies execution order, not Vulkan memory
|
|
+ // visibility for attachment, shader, and transfer accesses.
|
|
+ inline void finishTrackedEncoder(VkCommandBuffer commandBuffer) {
|
|
+ VkMemoryBarrier barrier {};
|
|
+ barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
|
+ barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
+ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
+ DynamicVK::vkCmdPipelineBarrier(commandBuffer,
|
|
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
+ 0, 1, &barrier, 0, nullptr, 0, nullptr);
|
|
+ }
|
|
+ // First few times the app leaves a bound-by-the-shader resource unset: tells us which slots to look at.
|
|
+ inline void logUnboundResource(const char* kind, size_t binding, size_t index, size_t available) {
|
|
+ static std::atomic<unsigned> logged {0};
|
|
+ if (logged.fetch_add(1) >= 30) {
|
|
+ return;
|
|
+ }
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "unbound-%s: binding=%zu index=%zu (have %zu)\n", kind, binding, index, available);
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Descriptor writes are consumed synchronously by vkUpdateDescriptorSets.
|
|
+ // Keep common shader layouts on the stack; retain a sized heap fallback
|
|
+ // for shaders exceeding the inline capacity. Never move storage after
|
|
+ // pointers to descriptor infos have been published.
|
|
+ template<class T, size_t capacity = 64> class DescriptorScratch {
|
|
+ std::array<T, capacity> _inline;
|
|
+ std::vector<T> _large;
|
|
+ T* _data;
|
|
+ size_t _size = 0;
|
|
+ public:
|
|
+ explicit DescriptorScratch(size_t maximum) {
|
|
+ if (maximum > capacity) _large.resize(maximum);
|
|
+ _data = maximum > capacity ? _large.data() : _inline.data();
|
|
+ }
|
|
+ T& emplace_back() { T& value = _data[_size++]; value = T{}; return value; }
|
|
+ void push_back(const T& value) { _data[_size++] = value; }
|
|
+ void pop_back() { --_size; }
|
|
+ T* data() { return _data; }
|
|
+ size_t size() const { return _size; }
|
|
+ };
|
|
+
|
|
+ // Per-encoder binding tables are indexed by Metal slot and start empty in every render pass. Growing them one slot at
|
|
+ // a time reallocated (and moved) them for each new highest index, through the application's own operator new, which
|
|
+ // League replaces with an allocator that takes a mutex. Grow straight to a size that covers the usual slots.
|
|
+ template<class Vector> inline void growToIndex(Vector& vector, size_t index) {
|
|
+ if (vector.size() > index) return;
|
|
+ if (vector.capacity() <= index) vector.reserve(index < 16 ? 16 : index + 1);
|
|
+ vector.resize(index + 1);
|
|
+ }
|
|
+
|
|
+ template<class Base, class Private> struct ResolvedResource {
|
|
+ Base* object = nullptr;
|
|
+ Private* value = nullptr;
|
|
+ bool depthFeedback = false;
|
|
+ bool refresh(const std::shared_ptr<Base>& owner) {
|
|
+ if (object == owner.get()) return false;
|
|
+ object = owner.get();
|
|
+ value = dynamic_cast<Private*>(object);
|
|
+ return true;
|
|
+ }
|
|
+ };
|
|
+ struct ResolvedFunctionResources {
|
|
+ std::vector<ResolvedResource<Buffer, PrivateBuffer>> buffers;
|
|
+ std::vector<ResolvedResource<Texture, PrivateTexture>> textures;
|
|
+ std::vector<ResolvedResource<SamplerState, PrivateSamplerState>> samplers;
|
|
+ };
|
|
+
|
|
+ template<class T> inline bool sameResourceOwner(const std::shared_ptr<T>& a, const std::shared_ptr<T>& b) {
|
|
+ return a.get() == b.get() && !a.owner_before(b) && !b.owner_before(a);
|
|
+ }
|
|
struct FunctionResources {
|
|
std::vector<std::pair<std::shared_ptr<Buffer>, size_t>> buffers;
|
|
std::vector<std::shared_ptr<Texture>> textures;
|
|
std::vector<std::shared_ptr<SamplerState>> samplers;
|
|
+ uint64_t revision = 0;
|
|
|
|
- void setBytes(std::shared_ptr<Device> device, const void* bytes, size_t length, size_t index) {
|
|
- // TODO: we can make this "Private" instead
|
|
- auto buf = device->newBuffer(bytes, length, ResourceOptions::StorageModeShared);
|
|
+ // Small inline constants (setBytes) are bump-allocated from shared chunks: a dedicated buffer per call meant one
|
|
+ // GPU memory allocation (and a kernel address-space update) per draw. Copies of this struct share the chunk, which
|
|
+ // keeps it alive for as long as the command buffer needs it; allocations never overlap.
|
|
+ std::shared_ptr<Buffer> byteChunk;
|
|
+ size_t byteChunkUsed = 0;
|
|
+ // bumped only by texture and sampler changes: most draws change buffers or offsets and nothing else
|
|
+ uint64_t imageRevision = 0;
|
|
|
|
- if (buffers.size() <= index) {
|
|
- buffers.resize(index + 1);
|
|
+ void setBytes(std::shared_ptr<Device> device, const void* bytes, size_t length, size_t index) {
|
|
+ constexpr size_t chunkSize = 256 * 1024;
|
|
+ constexpr size_t alignment = 256;
|
|
+
|
|
+ // setBytes storage is private and immutable until GPU retirement.
|
|
+ // Repeated identical constants can keep their existing address.
|
|
+ if (bindingReuseEnabled() && byteChunk && index < buffers.size() && buffers[index].first.get() == byteChunk.get() &&
|
|
+ length <= 4096 && buffers[index].second <= byteChunkUsed &&
|
|
+ length <= byteChunkUsed - buffers[index].second &&
|
|
+ (!length || !memcmp(static_cast<const char*>(byteChunk->contents()) + buffers[index].second, bytes, length)))
|
|
+ return;
|
|
+
|
|
+ std::shared_ptr<Buffer> buf;
|
|
+ size_t offset = 0;
|
|
+
|
|
+ if (length > chunkSize / 8) {
|
|
+ // TODO: we can make this "Private" instead
|
|
+ buf = device->newBuffer(bytes, length, ResourceOptions::StorageModeShared);
|
|
+ } else {
|
|
+ offset = (byteChunkUsed + alignment - 1) & ~(alignment - 1);
|
|
+ if (!byteChunk || offset + length > chunkSize) {
|
|
+ byteChunk = std::dynamic_pointer_cast<PrivateDevice>(device)->acquireScratchChunk();
|
|
+ offset = 0;
|
|
+ }
|
|
+ memcpy(static_cast<char*>(byteChunk->contents()) + offset, bytes, length);
|
|
+ byteChunkUsed = offset + length;
|
|
+ buf = byteChunk;
|
|
}
|
|
|
|
- buffers[index] = std::make_pair(buf, 0);
|
|
+ growToIndex(buffers, index);
|
|
+
|
|
+ buffers[index] = std::make_pair(std::move(buf), offset);
|
|
+ ++revision;
|
|
};
|
|
|
|
- void setBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) {
|
|
- if (buffers.size() <= index) {
|
|
- buffers.resize(index + 1);
|
|
- }
|
|
+ void setBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) {
|
|
+ growToIndex(buffers, index);
|
|
|
|
- buffers[index] = std::make_pair(buffer, offset);
|
|
+ const bool ownerChanged = !sameResourceOwner(buffers[index].first, buffer);
|
|
+ if (ownerChanged || buffers[index].second != offset) ++revision;
|
|
+ if (ownerChanged) buffers[index].first = buffer;
|
|
+ buffers[index].second = offset;
|
|
};
|
|
|
|
void setBufferOffset(size_t offset, size_t index) {
|
|
+ if (index >= buffers.size()) {
|
|
+ // Metal requires the buffer to be bound first; this used to write past the end of the vector
|
|
+ logUnboundResource("buffer-offset", index, index, buffers.size());
|
|
+ return;
|
|
+ }
|
|
+ if (buffers[index].second != offset) ++revision;
|
|
buffers[index].second = offset;
|
|
};
|
|
|
|
- void setSamplerState(std::shared_ptr<SamplerState> state, std::optional<std::pair<float, float>> lodClamps, size_t index) {
|
|
- if (samplers.size() <= index) {
|
|
- samplers.resize(index + 1);
|
|
- }
|
|
+ void setSamplerState(const std::shared_ptr<SamplerState>& state, std::optional<std::pair<float, float>> lodClamps, size_t index) {
|
|
+ growToIndex(samplers, index);
|
|
|
|
if (lodClamps) {
|
|
auto privateState = std::dynamic_pointer_cast<PrivateSamplerState>(state);
|
|
samplers[index] = privateState->cloneWithClamps(lodClamps->first, lodClamps->second);
|
|
+ ++revision; ++imageRevision;
|
|
} else {
|
|
- samplers[index] = state;
|
|
+ if (!sameResourceOwner(samplers[index], state)) { samplers[index] = state; ++revision; ++imageRevision; }
|
|
}
|
|
};
|
|
|
|
- void setTexture(std::shared_ptr<Texture> texture, size_t index) {
|
|
- if (textures.size() <= index) {
|
|
- textures.resize(index + 1);
|
|
- }
|
|
+ void setTexture(const std::shared_ptr<Texture>& texture, size_t index) {
|
|
+ growToIndex(textures, index);
|
|
|
|
- textures[index] = texture;
|
|
+ if (!sameResourceOwner(textures[index], texture)) { textures[index] = texture; ++revision; ++imageRevision; }
|
|
};
|
|
};
|
|
|
|
- template<size_t setCount>
|
|
- std::array<VkDescriptorSet, setCount> createDescriptorSets(std::array<VkDescriptorSetLayout, setCount>& setLayouts, VkDescriptorPool pool, std::shared_ptr<PrivateDevice> privateDevice, const std::array<std::reference_wrapper<const FunctionResources>, setCount>& funcResources, const std::array<std::reference_wrapper<const FunctionInfo>, setCount>& functionInfos, std::vector<std::shared_ptr<Buffer>>& keepAliveBuffers) {
|
|
- std::array<VkDescriptorSet, setCount> descriptorSets {};
|
|
-
|
|
- VkDescriptorSetAllocateInfo setAllocateInfo {};
|
|
- setAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
- setAllocateInfo.descriptorPool = pool;
|
|
- setAllocateInfo.descriptorSetCount = descriptorSets.size();
|
|
- setAllocateInfo.pSetLayouts = setLayouts.data();
|
|
-
|
|
- if (DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data()) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
+ static constexpr std::array<VkDescriptorPoolSize, 5> poolSizes {
|
|
+ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 },
|
|
+ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 512 },
|
|
+ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 512 },
|
|
+ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 512 },
|
|
+ VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLER, 512 },
|
|
+ };
|
|
|
|
- for (size_t i = 0; i < descriptorSets.size(); ++i) {
|
|
- std::vector<VkWriteDescriptorSet> writeDescSet;
|
|
- std::forward_list<VkDescriptorBufferInfo> bufInfos;
|
|
- std::forward_list<VkDescriptorImageInfo> imageInfos;
|
|
+ static inline VkDescriptorPool newDescriptorPool(std::shared_ptr<PrivateDevice> device) {
|
|
+ return device->acquireDescriptorPool();
|
|
+ }
|
|
|
|
- const FunctionResources& functionResources = funcResources[i];
|
|
- const FunctionInfo& funcInfo = functionInfos[i];
|
|
+ // Bump allocator for the small per-draw tables of buffer addresses (one dedicated buffer per table meant one GPU memory
|
|
+ // allocation per stage per draw).
|
|
+ struct AddressPool {
|
|
+ std::shared_ptr<Buffer> chunk;
|
|
+ size_t used = 0;
|
|
+ };
|
|
|
|
- if (functionResources.buffers.size() > 0) {
|
|
- std::vector<uint64_t> addresses;
|
|
+ struct BufferAddressTable {
|
|
+ VkBuffer buffer = VK_NULL_HANDLE;
|
|
+ uint32_t offset = 0;
|
|
+ VkDeviceSize range = 0;
|
|
+ };
|
|
+ inline BufferAddressTable makeBufferAddressTable(const FunctionResources& functionResources, const FunctionInfo& functionInfo,
|
|
+ const ResolvedFunctionResources* resolved, const std::shared_ptr<PrivateDevice>& privateDevice,
|
|
+ std::vector<std::shared_ptr<Buffer>>& keepAliveBuffers, AddressPool& addressPool,
|
|
+ const BufferAddressTable* previous = nullptr) {
|
|
+ DescriptorScratch<uint64_t> addresses(functionInfo.bindings.size());
|
|
|
|
// find the right buffer for each binding (using the binding index)
|
|
- for (size_t j = 0; j < funcInfo.bindings.size(); ++j) {
|
|
- auto& bindingInfo = funcInfo.bindings[j];
|
|
+ for (size_t j = 0; j < functionInfo.bindings.size(); ++j) {
|
|
+ auto& bindingInfo = functionInfo.bindings[j];
|
|
|
|
if (bindingInfo.type != Iridium::BindingType::Buffer) {
|
|
continue;
|
|
}
|
|
|
|
- if (bindingInfo.index >= functionResources.buffers.size()) {
|
|
- addresses.push_back(0);
|
|
+ PrivateBuffer* privateBuf = nullptr;
|
|
+ if (resolved && bindingInfo.index < resolved->buffers.size())
|
|
+ privateBuf = resolved->buffers[bindingInfo.index].value;
|
|
+ else if (bindingInfo.index < functionResources.buffers.size())
|
|
+ privateBuf = dynamic_cast<PrivateBuffer*>(functionResources.buffers[bindingInfo.index].first.get());
|
|
+ if (!privateBuf) {
|
|
+ // address 0 here was dereferenced by the shader: GPU page fault at 0x0, ring timeout, GPU reset
|
|
+ logUnboundResource("buffer", j, bindingInfo.index, functionResources.buffers.size());
|
|
+ addresses.push_back(privateDevice->fallbackBufferAddress());
|
|
continue;
|
|
}
|
|
-
|
|
- auto privateBuf = std::dynamic_pointer_cast<PrivateBuffer>(functionResources.buffers[bindingInfo.index].first);
|
|
addresses.push_back(privateBuf->gpuAddress() + functionResources.buffers[bindingInfo.index].second);
|
|
}
|
|
|
|
- auto addressBuffer = privateDevice->newBuffer(addresses.data(), functionResources.buffers.size() * 8, ResourceOptions::StorageModeShared);
|
|
- auto privateAddrBuf = std::dynamic_pointer_cast<PrivateBuffer>(addressBuffer);
|
|
+ constexpr size_t chunkSize = 256 * 1024;
|
|
+ constexpr size_t alignment = 256;
|
|
+ const size_t tableSize = (addresses.size() * sizeof(uint64_t) + 15) & ~size_t(15);
|
|
+ // Reuse immutable table bytes when setters changed unused slots
|
|
+ // or selected an equivalent GPU address. Do not retain storage
|
|
+ // from another chunk: its lifetime belongs to an earlier table.
|
|
+ if (bindingReuseEnabled() && previous && addressPool.chunk && previous->range == tableSize &&
|
|
+ previous->offset <= chunkSize && tableSize <= chunkSize - previous->offset &&
|
|
+ previous->buffer == static_cast<PrivateBuffer*>(addressPool.chunk.get())->buffer() &&
|
|
+ !memcmp(static_cast<const char*>(addressPool.chunk->contents()) + previous->offset,
|
|
+ addresses.data(), addresses.size() * sizeof(uint64_t)))
|
|
+ return *previous;
|
|
+ size_t tableOffset = (addressPool.used + alignment - 1) & ~(alignment - 1);
|
|
+ if (!addressPool.chunk || tableOffset + tableSize > chunkSize) {
|
|
+ addressPool.chunk = privateDevice->acquireScratchChunk();
|
|
+ // we need to keep the chunk alive until the operation is completed
|
|
+ keepAliveBuffers.push_back(addressPool.chunk);
|
|
+ tableOffset = 0;
|
|
+ }
|
|
+ memcpy(static_cast<char*>(addressPool.chunk->contents()) + tableOffset, addresses.data(), addresses.size() * sizeof(uint64_t));
|
|
+ addressPool.used = tableOffset + tableSize;
|
|
+ // plain pointer: a shared_ptr cast here was an atomic reference count round trip per stage per draw
|
|
+ auto* privateAddressBuffer = static_cast<PrivateBuffer*>(addressPool.chunk.get());
|
|
|
|
- // we need to keep this buffer alive until the operation is completed
|
|
- keepAliveBuffers.push_back(addressBuffer);
|
|
+ return {privateAddressBuffer->buffer(), static_cast<uint32_t>(tableOffset), tableSize};
|
|
+ }
|
|
|
|
- auto& info = bufInfos.emplace_front();
|
|
- info.buffer = privateAddrBuf->buffer();
|
|
- info.offset = 0;
|
|
- info.range = VK_WHOLE_SIZE;
|
|
+ template<size_t setCount>
|
|
+ std::array<VkDescriptorSet, setCount> createDescriptorSets(std::array<VkDescriptorSetLayout, setCount>& setLayouts, VkDescriptorPool& pool, std::vector<VkDescriptorPool>& retiredPools, std::shared_ptr<PrivateDevice> privateDevice, const std::array<std::reference_wrapper<const FunctionResources>, setCount>& funcResources, const std::array<std::reference_wrapper<const FunctionInfo>, setCount>& functionInfos, std::vector<std::shared_ptr<Buffer>>& keepAliveBuffers, AddressPool& addressPool, const std::array<const ResolvedFunctionResources*, setCount>& resolved = {}, const std::array<const BufferAddressTable*, setCount>& dynamicTables = {}) {
|
|
+ std::array<VkDescriptorSet, setCount> descriptorSets {};
|
|
+
|
|
+ VkDescriptorSetAllocateInfo setAllocateInfo {};
|
|
+ setAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
|
|
+ setAllocateInfo.descriptorPool = pool;
|
|
+ setAllocateInfo.descriptorSetCount = descriptorSets.size();
|
|
+ setAllocateInfo.pSetLayouts = setLayouts.data();
|
|
+
|
|
+ auto result = DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data());
|
|
+ if (result == VK_ERROR_OUT_OF_POOL_MEMORY || result == VK_ERROR_FRAGMENTED_POOL) {
|
|
+ // Keep previous sets alive until this encoder's GPU work completes.
|
|
+ auto next = newDescriptorPool(privateDevice);
|
|
+ retiredPools.push_back(pool);
|
|
+ pool = next;
|
|
+ setAllocateInfo.descriptorPool = pool;
|
|
+ result = DynamicVK::vkAllocateDescriptorSets(privateDevice->device(), &setAllocateInfo, descriptorSets.data());
|
|
+ }
|
|
+ if (result != VK_SUCCESS) throw std::runtime_error("Descriptor set allocation failed: " + std::to_string(result));
|
|
+
|
|
+
|
|
+ for (size_t i = 0; i < descriptorSets.size(); ++i) {
|
|
+ const FunctionResources& functionResources = funcResources[i];
|
|
+ const FunctionInfo& funcInfo = functionInfos[i];
|
|
+ DescriptorScratch<VkWriteDescriptorSet> writeDescSet(funcInfo.bindings.size() + 1);
|
|
+ VkDescriptorBufferInfo bufferInfo {};
|
|
+ DescriptorScratch<VkDescriptorImageInfo> imageInfos(funcInfo.bindings.size());
|
|
+
|
|
+ if (std::any_of(funcInfo.bindings.begin(), funcInfo.bindings.end(), [](const auto& binding) { return binding.type == Iridium::BindingType::Buffer; })) {
|
|
+ const auto table = dynamicTables[i] ? *dynamicTables[i] :
|
|
+ makeBufferAddressTable(functionResources, funcInfo, resolved[i], privateDevice, keepAliveBuffers, addressPool);
|
|
+ auto& info = bufferInfo;
|
|
+ info.buffer = table.buffer;
|
|
+ info.offset = dynamicTables[i] ? 0 : table.offset;
|
|
+ info.range = table.range;
|
|
|
|
auto& descSet = writeDescSet.emplace_back();
|
|
descSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
descSet.dstSet = descriptorSets[i];
|
|
descSet.dstBinding = 0;
|
|
descSet.dstArrayElement = 0;
|
|
- descSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
+ descSet.descriptorType = dynamicTables[i] ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
descSet.descriptorCount = 1;
|
|
descSet.pBufferInfo = &info;
|
|
}
|
|
@@ -136,17 +315,27 @@ namespace Indium {
|
|
auto& bindingInfo = funcInfo.bindings[j];
|
|
|
|
if (bindingInfo.type == Iridium::BindingType::Texture) {
|
|
- if (bindingInfo.index >= functionResources.textures.size()) {
|
|
- continue;
|
|
+ PrivateTexture* privateTexture = nullptr;
|
|
+ if (resolved[i] && bindingInfo.index < resolved[i]->textures.size())
|
|
+ privateTexture = resolved[i]->textures[bindingInfo.index].value;
|
|
+ else if (bindingInfo.index < functionResources.textures.size())
|
|
+ privateTexture = dynamic_cast<PrivateTexture*>(functionResources.textures[bindingInfo.index].get());
|
|
+
|
|
+ auto& info = imageInfos.emplace_back();
|
|
+ if (privateTexture) {
|
|
+ info.imageView = privateTexture->sampledImageView();
|
|
+ info.imageLayout = privateTexture->imageLayout();
|
|
+ } else {
|
|
+ // leaving the descriptor unwritten lets the shader read whatever the set memory held
|
|
+ logUnboundResource("texture", j, bindingInfo.index, functionResources.textures.size());
|
|
+ if (!privateDevice->nullDescriptorEnabled()) {
|
|
+ imageInfos.pop_back();
|
|
+ continue;
|
|
+ }
|
|
+ info.imageView = VK_NULL_HANDLE;
|
|
+ info.imageLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
}
|
|
|
|
- auto texture = functionResources.textures[bindingInfo.index];
|
|
- auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
|
-
|
|
- auto& info = imageInfos.emplace_front();
|
|
- info.imageView = privateTexture->imageView();
|
|
- info.imageLayout = privateTexture->imageLayout();
|
|
-
|
|
auto& descSet = writeDescSet.emplace_back();
|
|
descSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
|
|
descSet.dstSet = descriptorSets[i];
|
|
@@ -156,19 +345,21 @@ namespace Indium {
|
|
descSet.descriptorCount = 1;
|
|
descSet.pImageInfo = &info;
|
|
} else if (bindingInfo.type == Iridium::BindingType::Sampler) {
|
|
- bool embeddedSampler = false;
|
|
-
|
|
- if (bindingInfo.index == SIZE_MAX) {
|
|
- // this binding uses an embedded sampler
|
|
- embeddedSampler = true;
|
|
- } else if (bindingInfo.index >= functionResources.samplers.size()) {
|
|
- continue;
|
|
+ PrivateSamplerState* privateSampler = nullptr;
|
|
+ if (bindingInfo.index == SIZE_MAX) {
|
|
+ privateSampler = dynamic_cast<PrivateSamplerState*>(funcInfo.embeddedSamplerStates[bindingInfo.embeddedSamplerIndex].get());
|
|
+ } else if (resolved[i] && bindingInfo.index < resolved[i]->samplers.size()) {
|
|
+ privateSampler = resolved[i]->samplers[bindingInfo.index].value;
|
|
+ } else if (bindingInfo.index < functionResources.samplers.size()) {
|
|
+ privateSampler = dynamic_cast<PrivateSamplerState*>(functionResources.samplers[bindingInfo.index].get());
|
|
+ }
|
|
+ if (!privateSampler) {
|
|
+ logUnboundResource("sampler", j, bindingInfo.index, functionResources.samplers.size());
|
|
+ privateSampler = dynamic_cast<PrivateSamplerState*>(privateDevice->fallbackSampler().get());
|
|
+ if (!privateSampler) continue;
|
|
}
|
|
|
|
- auto sampler = embeddedSampler ? funcInfo.embeddedSamplerStates[bindingInfo.embeddedSamplerIndex] : functionResources.samplers[bindingInfo.index];
|
|
- auto privateSampler = std::dynamic_pointer_cast<PrivateSamplerState>(sampler);
|
|
-
|
|
- auto& info = imageInfos.emplace_front();
|
|
+ auto& info = imageInfos.emplace_back();
|
|
info.sampler = privateSampler->sampler();
|
|
|
|
auto& descSet = writeDescSet.emplace_back();
|
|
@@ -188,10 +379,4 @@ namespace Indium {
|
|
return descriptorSets;
|
|
};
|
|
|
|
- static constexpr std::array<VkDescriptorPoolSize, 4> poolSizes {
|
|
- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 512 },
|
|
- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 512 },
|
|
- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 512 },
|
|
- VkDescriptorPoolSize { VK_DESCRIPTOR_TYPE_SAMPLER, 512 },
|
|
- };
|
|
};
|
|
diff --git a/private-include/indium/command-queue.private.hpp b/private-include/indium/command-queue.private.hpp
|
|
index edfc54e..6d58cf6 100644
|
|
--- a/private-include/indium/command-queue.private.hpp
|
|
+++ b/private-include/indium/command-queue.private.hpp
|
|
@@ -3,6 +3,8 @@
|
|
#include <indium/command-queue.hpp>
|
|
|
|
#include <vulkan/vulkan.h>
|
|
+#include <mutex>
|
|
+#include <vector>
|
|
|
|
namespace Indium {
|
|
class PrivateDevice;
|
|
@@ -22,5 +24,12 @@ namespace Indium {
|
|
INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice);
|
|
|
|
INDIUM_PROPERTY(VkCommandPool, c, C,ommandPool) = VK_NULL_HANDLE;
|
|
+ private:
|
|
+ std::mutex _poolMutex;
|
|
+ struct RecordingPool { VkCommandPool pool; VkCommandBuffer buffer; };
|
|
+ std::vector<RecordingPool> _availablePools;
|
|
+ public:
|
|
+ VkCommandPool acquireCommandPool(VkCommandBuffer& buffer);
|
|
+ void releaseCommandPool(VkCommandPool pool, VkCommandBuffer buffer);
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/compute-command-encoder.private.hpp b/private-include/indium/compute-command-encoder.private.hpp
|
|
index 8546daa..7e4993c 100644
|
|
--- a/private-include/indium/compute-command-encoder.private.hpp
|
|
+++ b/private-include/indium/compute-command-encoder.private.hpp
|
|
@@ -15,12 +15,14 @@ namespace Indium {
|
|
FunctionResources _functionResources;
|
|
std::shared_ptr<PrivateComputePipelineState> _pso;
|
|
VkDescriptorPool _pool = VK_NULL_HANDLE;
|
|
+ std::vector<VkDescriptorPool> _retiredPools;
|
|
|
|
// XXX: not sure if we actually need to store pipelines until the command buffer finishes executing,
|
|
// but let's do it just in case.
|
|
std::vector<VkPipeline> _savedPipelines;
|
|
std::vector<FunctionResources> _savedFunctionResources;
|
|
std::vector<std::shared_ptr<Buffer>> _keepAliveBuffers;
|
|
+ AddressPool _addressPool;
|
|
|
|
void updateBindings();
|
|
|
|
diff --git a/private-include/indium/device.private.hpp b/private-include/indium/device.private.hpp
|
|
index 58f1794..be7dbdc 100644
|
|
--- a/private-include/indium/device.private.hpp
|
|
+++ b/private-include/indium/device.private.hpp
|
|
@@ -13,6 +13,8 @@
|
|
#include <functional>
|
|
#include <atomic>
|
|
#include <optional>
|
|
+#include <unordered_map>
|
|
+#include <indium/render-pass-key.private.hpp>
|
|
|
|
namespace Indium {
|
|
class PrivateDevice;
|
|
@@ -25,6 +27,8 @@ namespace Indium {
|
|
class PrivateDevice: public Device, public std::enable_shared_from_this<PrivateDevice> {
|
|
private:
|
|
|
|
+ std::mutex _scratchMutex;
|
|
+ std::vector<std::shared_ptr<Buffer>> _scratchFree;
|
|
std::mutex _eventLoopMutex;
|
|
std::mutex _pollingMutex;
|
|
|
|
@@ -59,6 +63,8 @@ namespace Indium {
|
|
}
|
|
|
|
virtual std::string name() const override;
|
|
+ virtual bool supportsDepth24Stencil8PixelFormat() override;
|
|
+
|
|
virtual std::shared_ptr<CommandQueue> newCommandQueue() override;
|
|
virtual std::shared_ptr<RenderPipelineState> newRenderPipelineState(const RenderPipelineDescriptor& descriptor) override;
|
|
virtual std::shared_ptr<ComputePipelineState> newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> reflection) override;
|
|
@@ -73,6 +79,12 @@ namespace Indium {
|
|
virtual void pollEvents(uint64_t timeoutNanoseconds) override;
|
|
virtual void wakeupEventLoop() override;
|
|
|
|
+ // device address of a zero-filled 64 KiB buffer; never 0
|
|
+ uint64_t fallbackBufferAddress();
|
|
+ std::shared_ptr<SamplerState> fallbackSampler();
|
|
+ // VK_EXT_robustness2 nullDescriptor was enabled: null image views / vertex buffers are legal
|
|
+ bool nullDescriptorEnabled() const { return _nullDescriptor; }
|
|
+
|
|
void waitForSemaphore(VkSemaphore semaphore, uint64_t targetValue, std::function<void()> callback);
|
|
|
|
/**
|
|
@@ -86,6 +98,11 @@ namespace Indium {
|
|
* Similar to getTimelineSemaphore(), but wraps it in a shared pointer
|
|
* that automatically releases the semaphore when all references to it die out.
|
|
*/
|
|
+ // Fixed-size scratch buffers for per-draw constants and address tables. They go back to a free list when the last
|
|
+ // reference is dropped (i.e. when the command buffers that used them are done), instead of being freed and re-allocated.
|
|
+ static constexpr size_t scratchChunkSize = 256 * 1024;
|
|
+ std::shared_ptr<Buffer> acquireScratchChunk();
|
|
+
|
|
std::shared_ptr<TimelineSemaphore> getWrappedTimelineSemaphore();
|
|
|
|
BinarySemaphore getBinarySemaphore(bool exportable = false);
|
|
@@ -111,5 +128,34 @@ namespace Indium {
|
|
|
|
INDIUM_PROPERTY(VkPhysicalDeviceMemoryProperties, m, M,emoryProperties);
|
|
INDIUM_PROPERTY_READONLY(Feature, f, F,eatures);
|
|
+
|
|
+ private:
|
|
+ // These stay LAST: QuartzCore and Metal are compiled against this layout (they read _device and friends at fixed
|
|
+ // offsets), so adding members earlier in the class breaks the installed frameworks.
|
|
+ // Stand-ins for resources the app never bound (see createDescriptorSets): shaders read buffers through raw device
|
|
+ // addresses, so an unbound index used to become address 0 and faulted the GPU there.
|
|
+ std::mutex _fallbackMutex;
|
|
+ std::shared_ptr<Buffer> _fallbackBuffer;
|
|
+ std::shared_ptr<SamplerState> _fallbackSampler;
|
|
+ bool _nullDescriptor = false;
|
|
+ std::mutex _graphicsQueueMutex;
|
|
+ std::mutex _oneshotCommandPoolMutex;
|
|
+ public:
|
|
+ std::mutex& graphicsQueueMutex() { return _graphicsQueueMutex; }
|
|
+ std::mutex& oneshotCommandPoolMutex() { return _oneshotCommandPoolMutex; }
|
|
+ VkDescriptorPool acquireDescriptorPool();
|
|
+ void releaseDescriptorPool(VkDescriptorPool pool);
|
|
+ // Cached handles belong to the device and remain alive through all GPU uses.
|
|
+ std::pair<VkRenderPass, bool> acquireRenderPass(const VkRenderPassCreateInfo& info);
|
|
+ // Memory type index for a buffer or image of the given storage mode, or SIZE_MAX. Non-virtual and without
|
|
+ // state in the class, so the layout above is unchanged.
|
|
+ size_t findMemoryType(uint32_t memoryTypeBits, StorageMode storageMode, bool image);
|
|
+ private:
|
|
+ std::mutex _descriptorPoolMutex;
|
|
+ std::vector<VkDescriptorPool> _availableDescriptorPools;
|
|
+ std::mutex _timelineSemaphorePoolMutex;
|
|
+ std::vector<std::pair<VkSemaphore, uint64_t>> _availableTimelineSemaphores;
|
|
+ std::mutex _renderPassMutex;
|
|
+ std::unordered_map<RenderPassKey, VkRenderPass, RenderPassKey::Hash> _renderPasses;
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/dynamic-vk.hpp b/private-include/indium/dynamic-vk.hpp
|
|
index 4957eca..fb2a315 100644
|
|
--- a/private-include/indium/dynamic-vk.hpp
|
|
+++ b/private-include/indium/dynamic-vk.hpp
|
|
@@ -6,6 +6,9 @@
|
|
#include <indium/instance.private.hpp>
|
|
|
|
#include <mutex>
|
|
+#include <atomic>
|
|
+#include <stdexcept>
|
|
+#include <string>
|
|
|
|
namespace Indium {
|
|
namespace DynamicVK {
|
|
@@ -14,6 +17,8 @@ namespace Indium {
|
|
extern PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties;
|
|
|
|
bool init();
|
|
+ void beginDiagnosticCapture(VkInstance instance);
|
|
+ void endDiagnosticCapture(bool presented);
|
|
void finit();
|
|
|
|
/**
|
|
@@ -24,7 +29,7 @@ namespace Indium {
|
|
bool eagerlyResolveRequired();
|
|
|
|
struct DynamicFunctionBase {
|
|
- void* pointer;
|
|
+ std::atomic<void*> pointer {nullptr};
|
|
std::once_flag _resolveFlag;
|
|
const char* name;
|
|
|
|
@@ -33,10 +38,11 @@ namespace Indium {
|
|
{};
|
|
|
|
bool resolve() {
|
|
+ if (pointer.load(std::memory_order_acquire)) return true;
|
|
std::call_once(_resolveFlag, [&]() {
|
|
- pointer = reinterpret_cast<void*>(vkGetInstanceProcAddr(globalInstance, name));
|
|
+ pointer.store(reinterpret_cast<void*>(vkGetInstanceProcAddr(globalInstance, name)), std::memory_order_release);
|
|
});
|
|
- return !!pointer;
|
|
+ return pointer.load(std::memory_order_acquire) != nullptr;
|
|
};
|
|
|
|
bool isAvailable() {
|
|
@@ -58,16 +64,20 @@ namespace Indium {
|
|
{};
|
|
|
|
Ret operator ()(Arg... arg) {
|
|
- if (!resolve()) {
|
|
- throw std::runtime_error(std::string("Failed to resolve ") + name);
|
|
- }
|
|
- Ret (*func)(Arg...) = reinterpret_cast<decltype(func)>(pointer);
|
|
+ void* address = pointer.load(std::memory_order_acquire);
|
|
+ if (!address) {
|
|
+ if (!resolve()) throw std::runtime_error(std::string("Failed to resolve ") + name);
|
|
+ address = pointer.load(std::memory_order_acquire);
|
|
+ }
|
|
+ Ret (*func)(Arg...) = reinterpret_cast<decltype(func)>(address);
|
|
return func(std::forward<Arg>(arg)...);
|
|
};
|
|
};
|
|
|
|
#define INDIUM_DYNAMICVK_FUNCTION_FOREACH(_macro) \
|
|
_macro(vkAcquireNextImageKHR) \
|
|
+ _macro(vkResetCommandPool) \
|
|
+ _macro(vkResetDescriptorPool) \
|
|
_macro(vkAllocateCommandBuffers) \
|
|
_macro(vkAllocateDescriptorSets) \
|
|
_macro(vkAllocateMemory) \
|
|
@@ -88,6 +98,10 @@ namespace Indium {
|
|
_macro(vkCmdDraw) \
|
|
_macro(vkCmdDrawIndexed) \
|
|
_macro(vkCmdEndRenderPass) \
|
|
+ _macro(vkCmdResetQueryPool) \
|
|
+ _macro(vkCmdWriteTimestamp) \
|
|
+ _macro(vkCreateQueryPool) \
|
|
+ _macro(vkGetQueryPoolResults) \
|
|
_macro(vkCmdFillBuffer) \
|
|
_macro(vkCmdPipelineBarrier) \
|
|
_macro(vkCmdSetBlendConstants) \
|
|
@@ -157,6 +171,7 @@ namespace Indium {
|
|
_macro(vkGetImageMemoryRequirements) \
|
|
_macro(vkGetPhysicalDeviceFeatures2) \
|
|
_macro(vkGetPhysicalDeviceMemoryProperties) \
|
|
+ _macro(vkGetPhysicalDeviceFormatProperties) \
|
|
_macro(vkGetPhysicalDeviceProperties) \
|
|
_macro(vkGetPhysicalDeviceProperties2) \
|
|
_macro(vkGetPhysicalDeviceQueueFamilyProperties) \
|
|
diff --git a/private-include/indium/library.private.hpp b/private-include/indium/library.private.hpp
|
|
index 8752aa2..faf50b2 100644
|
|
--- a/private-include/indium/library.private.hpp
|
|
+++ b/private-include/indium/library.private.hpp
|
|
@@ -26,6 +26,7 @@ namespace Indium {
|
|
std::vector<Iridium::BindingInfo> bindings;
|
|
std::vector<Iridium::EmbeddedSampler> embeddedSamplers;
|
|
std::vector<std::shared_ptr<SamplerState>> embeddedSamplerStates;
|
|
+ std::vector<Iridium::ArgumentInfo> arguments;
|
|
};
|
|
|
|
class PrivateFunction: public Function {
|
|
@@ -33,6 +34,8 @@ namespace Indium {
|
|
PrivateFunction(std::shared_ptr<PrivateLibrary> library, const std::string& name, const FunctionInfo& functionInfo);
|
|
|
|
virtual std::shared_ptr<Device> device() override;
|
|
+ virtual const std::vector<Iridium::ArgumentInfo>& argumentInfo() const override;
|
|
+ virtual FunctionType functionType() const override { return _functionInfo.functionType; }
|
|
|
|
INDIUM_PROPERTY_READONLY_OBJECT(PrivateDevice, p, P,rivateDevice);
|
|
|
|
diff --git a/private-include/indium/pipeline.private.hpp b/private-include/indium/pipeline.private.hpp
|
|
index a43f532..6f2d7dd 100644
|
|
--- a/private-include/indium/pipeline.private.hpp
|
|
+++ b/private-include/indium/pipeline.private.hpp
|
|
@@ -33,7 +33,7 @@ namespace Indium {
|
|
}
|
|
};
|
|
|
|
- void processFunction(std::shared_ptr<PrivateFunction> function, size_t layoutIndex) {
|
|
+ void processFunction(std::shared_ptr<PrivateFunction> function, size_t layoutIndex, bool dynamicBuffers = false) {
|
|
size_t index = 0;
|
|
std::vector<VkDescriptorSetLayoutBinding> bindings;
|
|
|
|
@@ -61,7 +61,7 @@ namespace Indium {
|
|
// we need a UBO to store the buffer addresses
|
|
auto& binding = bindings.emplace_back();
|
|
binding.binding = 0;
|
|
- binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
+ binding.descriptorType = dynamicBuffers ? VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC : VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
|
|
binding.descriptorCount = 1;
|
|
binding.stageFlags = functionTypeToVkShaderStageFlags(function->functionInfo().functionType);
|
|
}
|
|
diff --git a/private-include/indium/render-command-encoder.private.hpp b/private-include/indium/render-command-encoder.private.hpp
|
|
index a41b951..2f670e9 100644
|
|
--- a/private-include/indium/render-command-encoder.private.hpp
|
|
+++ b/private-include/indium/render-command-encoder.private.hpp
|
|
@@ -5,12 +5,15 @@
|
|
#include <indium/sampler.private.hpp>
|
|
#include <indium/device.hpp>
|
|
#include <indium/command-encoder.private.hpp>
|
|
+#include <indium/resource-owner-cache.private.hpp>
|
|
|
|
#include <vulkan/vulkan.h>
|
|
|
|
#include <array>
|
|
#include <variant>
|
|
#include <vector>
|
|
+#include <unordered_set>
|
|
+#include <unordered_map>
|
|
|
|
namespace Indium {
|
|
class PrivateCommandBuffer;
|
|
@@ -29,19 +32,41 @@ namespace Indium {
|
|
VkFramebuffer _framebuffer = VK_NULL_HANDLE;
|
|
VkRenderPass _renderPass = VK_NULL_HANDLE;
|
|
VkDescriptorPool _pool = VK_NULL_HANDLE;
|
|
+ std::vector<VkDescriptorPool> _retiredPools;
|
|
|
|
std::vector<FunctionResources> _savedFunctionResources;
|
|
|
|
std::array<FunctionResources, 2> _functionResources {};
|
|
std::vector<std::shared_ptr<Buffer>> _keepAliveBuffers;
|
|
-
|
|
- void updateBindings();
|
|
+ AddressPool _addressPool;
|
|
+ std::array<VkDescriptorSetLayout, 2> _boundLayouts {};
|
|
+ std::array<VkDescriptorSet, 2> _boundDescriptorSets {};
|
|
+ std::array<FunctionResources, 2> _boundResources {};
|
|
+ std::vector<std::shared_ptr<PrivateRenderPipelineState>> _keepAlivePipelineStates;
|
|
+
|
|
+ // A draw that samples the depth texture it has attached (soft-particle style depth fade) must not also write
|
|
+ // depth: that is a feedback loop, and writing makes later draws fail their depth test under the effect (the
|
|
+ // ward preview hid the lower half of the champion). Kept last: other frameworks read this class by layout.
|
|
+ std::shared_ptr<PrivateTexture> _depthRoot;
|
|
+ bool _depthWriteRequested = false;
|
|
+ bool _depthWriteSuppressed = false;
|
|
+ // Unique owners keep every resource referenced by earlier draws alive.
|
|
+ std::unordered_set<const void*> _retainedResourcePointers;
|
|
+ std::vector<std::shared_ptr<void>> _retainedResourceOwners;
|
|
+
|
|
+ void updateBindings(VkCommandBuffer commandBuffer);
|
|
|
|
public:
|
|
PrivateRenderCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer, const RenderPassDescriptor& descriptor);
|
|
~PrivateRenderCommandEncoder();
|
|
-
|
|
- virtual void setRenderPipelineState(std::shared_ptr<RenderPipelineState> renderPipelineState) override;
|
|
+ bool hasHeavyRetirement() const {
|
|
+ return _resolvedBufferOwners.size() + _resolvedTextureOwners.size() +
|
|
+ _pipelineOwners.size() + _retainedResourceOwners.size() >= 64 ||
|
|
+ !_retiredPools.empty();
|
|
+ }
|
|
+
|
|
+ virtual void memoryBarrier(size_t scope, RenderStages after, RenderStages before) override;
|
|
+ virtual void setRenderPipelineState(const std::shared_ptr<RenderPipelineState>& renderPipelineState) override;
|
|
virtual void setFrontFacingWinding(Winding frontFaceWinding) override;
|
|
virtual void setCullMode(CullMode cullMode) override;
|
|
virtual void setDepthBias(float depthBias, float slopeScale, float clamp) override;
|
|
@@ -53,7 +78,7 @@ namespace Indium {
|
|
virtual void setScissorRects(const ScissorRect* scissorRects, size_t count) override;
|
|
virtual void setScissorRects(const std::vector<ScissorRect>& scissorRects) override;
|
|
virtual void setBlendColor(float red, float green, float blue, float alpha) override;
|
|
- virtual void setDepthStencilState(std::shared_ptr<DepthStencilState> state) override;
|
|
+ virtual void setDepthStencilState(const std::shared_ptr<DepthStencilState>& state) override;
|
|
virtual void setTriangleFillMode(TriangleFillMode triangleFillMode) override;
|
|
virtual void setStencilReferenceValue(uint32_t value) override;
|
|
virtual void setStencilReferenceValue(uint32_t front, uint32_t back) override;
|
|
@@ -63,30 +88,30 @@ namespace Indium {
|
|
virtual void drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount) override;
|
|
virtual void drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount) override;
|
|
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) override;
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount) override;
|
|
- virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset) override;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) override;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount) override;
|
|
+ virtual void drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset) override;
|
|
|
|
virtual void setVertexBytes(const void* bytes, size_t length, size_t index) override;
|
|
- virtual void setVertexBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) override;
|
|
+ virtual void setVertexBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) override;
|
|
virtual void setVertexBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) override;
|
|
virtual void setVertexBufferOffset(size_t offset, size_t index) override;
|
|
- virtual void setVertexSamplerState(std::shared_ptr<SamplerState> state, size_t index) override;
|
|
- virtual void setVertexSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) override;
|
|
+ virtual void setVertexSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) override;
|
|
+ virtual void setVertexSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) override;
|
|
virtual void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) override;
|
|
virtual void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, const std::vector<float>& lodMinClamps, const std::vector<float>& lodMaxClamps, Range<size_t> range) override;
|
|
- virtual void setVertexTexture(std::shared_ptr<Texture> texture, size_t index) override;
|
|
+ virtual void setVertexTexture(const std::shared_ptr<Texture>& texture, size_t index) override;
|
|
virtual void setVertexTextures(const std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) override;
|
|
|
|
virtual void setFragmentBytes(const void* bytes, size_t length, size_t index) override;
|
|
- virtual void setFragmentBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) override;
|
|
+ virtual void setFragmentBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) override;
|
|
virtual void setFragmentBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) override;
|
|
virtual void setFragmentBufferOffset(size_t offset, size_t index) override;
|
|
- virtual void setFragmentSamplerState(std::shared_ptr<SamplerState> state, size_t index) override;
|
|
- virtual void setFragmentSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) override;
|
|
+ virtual void setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) override;
|
|
+ virtual void setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) override;
|
|
virtual void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) override;
|
|
virtual void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, const std::vector<float>& lodMinClamps, const std::vector<float>& lodMaxClamps, Range<size_t> range) override;
|
|
- virtual void setFragmentTexture(std::shared_ptr<Texture> texture, size_t index) override;
|
|
+ virtual void setFragmentTexture(const std::shared_ptr<Texture>& texture, size_t index) override;
|
|
virtual void setFragmentTextures(std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) override;
|
|
|
|
virtual void useResource(std::shared_ptr<Resource> resource, ResourceUsage usage, RenderStages stages) override;
|
|
@@ -95,8 +120,89 @@ namespace Indium {
|
|
virtual void useResources(const std::vector<std::shared_ptr<Resource>>& resources, ResourceUsage usage) override;
|
|
|
|
virtual void endEncoding() override;
|
|
+ // begins the Vulkan render pass on the first draw when the pass has no clear (see render-command-encoder.cpp)
|
|
+ void beginPassIfNeeded();
|
|
|
|
INDIUM_PROPERTY_OBJECT_VECTOR(Texture, r, R,eadOnlyTextures);
|
|
INDIUM_PROPERTY_OBJECT_VECTOR(Texture, r, R,eadWriteTextures);
|
|
+ private:
|
|
+ // Appended after existing properties: preserve every earlier field offset.
|
|
+ std::array<ResolvedFunctionResources, 2> _resolvedResources;
|
|
+ struct BindingKeys {
|
|
+ std::vector<std::pair<Buffer*, size_t>> buffers;
|
|
+ std::vector<Texture*> textures;
|
|
+ std::vector<SamplerState*> samplers;
|
|
+ };
|
|
+ std::array<BindingKeys, 2> _boundResourceKeys;
|
|
+ std::array<uint64_t, 2> _boundResourceRevisions {};
|
|
+ std::array<const FunctionInfo*, 2> _boundFunctionInfos {};
|
|
+ uint64_t _descriptorStageUpdates = 0;
|
|
+ uint64_t _descriptorStageReuses = 0;
|
|
+ std::array<BufferAddressTable, 2> _bufferAddressTables {};
|
|
+ std::array<uint32_t, 2> _lastDrawDynamicOffsets {};
|
|
+ VkPipeline _lastDrawPipeline = VK_NULL_HANDLE;
|
|
+ VkPrimitiveTopology _lastDrawTopology = VK_PRIMITIVE_TOPOLOGY_MAX_ENUM;
|
|
+ VkPipelineLayout _lastDescriptorPipelineLayout = VK_NULL_HANDLE;
|
|
+ std::array<VkDescriptorSet, 2> _lastDrawDescriptorSets {};
|
|
+ std::vector<VkBuffer> _lastVertexBuffers;
|
|
+ std::vector<VkDeviceSize> _lastVertexOffsets;
|
|
+ ResolvedResource<Buffer, PrivateBuffer> _lastIndexResource;
|
|
+ VkBuffer _lastIndexBuffer = VK_NULL_HANDLE;
|
|
+ VkDeviceSize _lastIndexOffset = 0;
|
|
+ VkIndexType _lastIndexType = VK_INDEX_TYPE_MAX_ENUM;
|
|
+
|
|
+ uint64_t _lastVertexResourceRevision = UINT64_MAX;
|
|
+ const PrivateRenderPipelineState* _lastVertexBindingPSO = nullptr;
|
|
+ ResourceOwnerCache<Buffer, PrivateBuffer> _resolvedBufferOwners;
|
|
+ void resolveBuffer(size_t stage, size_t index) {
|
|
+ auto& entries = _resolvedResources[stage].buffers;
|
|
+ growToIndex(entries, index);
|
|
+ auto& entry = entries[index];
|
|
+ const auto& owner = _functionResources[stage].buffers[index].first;
|
|
+ if (entry.object == owner.get()) return;
|
|
+ entry.object = owner.get();
|
|
+ if (!owner) { entry.value = nullptr; return; }
|
|
+ auto [found, inserted] = _resolvedBufferOwners.acquire(owner);
|
|
+ entry.value = found->value;
|
|
+ }
|
|
+ ResourceOwnerCache<RenderPipelineState, PrivateRenderPipelineState> _pipelineOwners;
|
|
+ ResourceOwnerCache<Texture, PrivateTexture> _resolvedTextureOwners;
|
|
+ void resolveTexture(size_t stage, size_t index) {
|
|
+ auto& entries = _resolvedResources[stage].textures;
|
|
+ growToIndex(entries, index);
|
|
+ auto& entry = entries[index];
|
|
+ const auto& owner = _functionResources[stage].textures[index];
|
|
+ if (entry.object == owner.get()) return;
|
|
+ entry.object = owner.get();
|
|
+ if (!owner) { entry.value = nullptr; entry.depthFeedback = false; return; }
|
|
+ auto [found, inserted] = _resolvedTextureOwners.acquire(owner);
|
|
+ auto& cached = *found;
|
|
+ if (inserted) {
|
|
+ auto* root = cached.value;
|
|
+ while (root) {
|
|
+ if (root == _depthRoot.get()) { cached.depthFeedback = true; break; }
|
|
+ root = dynamic_cast<PrivateTexture*>(root->parentTexture().get());
|
|
+ }
|
|
+ }
|
|
+ entry.value = cached.value;
|
|
+ entry.depthFeedback = cached.depthFeedback;
|
|
+ }
|
|
+ bool _renderPassDeviceOwned = false;
|
|
+ void resolveSampler(size_t stage, size_t index) {
|
|
+ auto& entries = _resolvedResources[stage].samplers;
|
|
+ growToIndex(entries, index);
|
|
+ const auto& owner = _functionResources[stage].samplers[index];
|
|
+ if (entries[index].refresh(owner) && owner && _retainedResourcePointers.insert(owner.get()).second)
|
|
+ _retainedResourceOwners.push_back(owner);
|
|
+ }
|
|
+
|
|
+ // appended last on purpose: lazily begun render pass
|
|
+ VkRenderPassBeginInfo _lazyBeginInfo {};
|
|
+ bool _passBegun = true;
|
|
+
|
|
+ // appended last: buffers-only fast path of updateBindings
|
|
+ std::array<uint64_t, 2> _boundImageRevisions {};
|
|
+ std::array<bool, 2> _boundKeysStale {};
|
|
+ std::array<bool, 2> _boundStageHasBuffers {};
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/texture.private.hpp b/private-include/indium/texture.private.hpp
|
|
index ccc27bb..e40e40c 100644
|
|
--- a/private-include/indium/texture.private.hpp
|
|
+++ b/private-include/indium/texture.private.hpp
|
|
@@ -88,6 +88,7 @@ namespace Indium {
|
|
|
|
virtual void precommit(std::shared_ptr<Indium::PrivateCommandBuffer> cmdbuf);
|
|
virtual bool needsExportablePresentationSemaphore() const;
|
|
+ virtual VkImageView sampledImageView() { return imageView(); }
|
|
};
|
|
|
|
class TextureView: public PrivateTexture {
|
|
@@ -102,6 +103,7 @@ namespace Indium {
|
|
Range<size_t> _levels;
|
|
Range<size_t> _layers;
|
|
TextureSwizzleChannels _swizzle;
|
|
+ VkImageView _sampledImageView = VK_NULL_HANDLE;
|
|
|
|
public:
|
|
TextureView(std::shared_ptr<PrivateTexture> original, PixelFormat pixelFormat, TextureType textureType, VkImageAspectFlags imageAspect, TextureSwizzleChannels swizzle, const Range<size_t>& levels, const Range<size_t>& layers);
|
|
@@ -125,6 +127,7 @@ namespace Indium {
|
|
virtual size_t parentRelativeSlice() const override;
|
|
|
|
virtual VkImageView imageView() override;
|
|
+ virtual VkImageView sampledImageView() override { return _sampledImageView ? _sampledImageView : imageView(); }
|
|
virtual VkImage image() override;
|
|
virtual std::shared_ptr<Device> device() override;
|
|
virtual VkImageLayout imageLayout() override;
|
|
@@ -150,6 +153,7 @@ namespace Indium {
|
|
VkImageView _imageView;
|
|
VkDeviceMemory _memory;
|
|
StorageMode _storageMode;
|
|
+ VkImageView _sampledImageView = VK_NULL_HANDLE;
|
|
|
|
public:
|
|
ConcreteTexture(std::shared_ptr<PrivateDevice> device, const TextureDescriptor& descriptor);
|
|
@@ -169,6 +173,7 @@ namespace Indium {
|
|
virtual TextureSwizzleChannels swizzle() const override;
|
|
|
|
virtual VkImageView imageView() override;
|
|
+ virtual VkImageView sampledImageView() override { return _sampledImageView ? _sampledImageView : imageView(); }
|
|
virtual VkImage image() override;
|
|
virtual VkImageLayout imageLayout() override;
|
|
virtual size_t vulkanArrayLength() const override;
|
|
diff --git a/private-include/indium/types.private.hpp b/private-include/indium/types.private.hpp
|
|
index d49b5f4..45bbd42 100644
|
|
--- a/private-include/indium/types.private.hpp
|
|
+++ b/private-include/indium/types.private.hpp
|
|
@@ -5,6 +5,7 @@
|
|
#include <vulkan/vulkan.h>
|
|
|
|
#include <memory>
|
|
+#include <stdexcept>
|
|
#include <vulkan/vulkan_core.h>
|
|
|
|
namespace Indium {
|
|
@@ -187,7 +188,7 @@ namespace Indium {
|
|
static constexpr size_t pixelFormatToByteCount(PixelFormat pixelFormat) {
|
|
switch (pixelFormat) {
|
|
case PixelFormat::Invalid: return 0;
|
|
- case PixelFormat::A8Unorm: return 0;
|
|
+ case PixelFormat::A8Unorm: return 1;
|
|
case PixelFormat::R8Unorm: return 1;
|
|
case PixelFormat::R8Unorm_sRGB: return 1;
|
|
case PixelFormat::R8Snorm: return 1;
|
|
@@ -244,6 +245,127 @@ namespace Indium {
|
|
}
|
|
};
|
|
|
|
+
|
|
+ struct PixelTransferBlock { size_t width, height, bytes; };
|
|
+
|
|
+ // Buffer pitches count bytes per block row; Vulkan copy pitches count texels.
|
|
+ static constexpr PixelTransferBlock pixelFormatTransferBlock(PixelFormat format) {
|
|
+ switch (format) {
|
|
+ case PixelFormat::BC1_RGBA:
|
|
+ case PixelFormat::BC1_RGBA_sRGB:
|
|
+ case PixelFormat::BC4_RUnorm:
|
|
+ case PixelFormat::BC4_RSnorm:
|
|
+ case PixelFormat::EAC_R11Unorm:
|
|
+ case PixelFormat::EAC_R11Snorm:
|
|
+ case PixelFormat::ETC2_RGB8:
|
|
+ case PixelFormat::ETC2_RGB8_sRGB:
|
|
+ case PixelFormat::ETC2_RGB8A1:
|
|
+ case PixelFormat::ETC2_RGB8A1_sRGB:
|
|
+ return { 4, 4, 8 };
|
|
+ case PixelFormat::BC2_RGBA:
|
|
+ case PixelFormat::BC2_RGBA_sRGB:
|
|
+ case PixelFormat::BC3_RGBA:
|
|
+ case PixelFormat::BC3_RGBA_sRGB:
|
|
+ case PixelFormat::BC5_RGUnorm:
|
|
+ case PixelFormat::BC5_RGSnorm:
|
|
+ case PixelFormat::BC6H_RGBFloat:
|
|
+ case PixelFormat::BC6H_RGBUfloat:
|
|
+ case PixelFormat::BC7_RGBAUnorm:
|
|
+ case PixelFormat::BC7_RGBAUnorm_sRGB:
|
|
+ case PixelFormat::EAC_RG11Unorm:
|
|
+ case PixelFormat::EAC_RG11Snorm:
|
|
+ case PixelFormat::EAC_RGBA8:
|
|
+ case PixelFormat::EAC_RGBA8_sRGB:
|
|
+ return { 4, 4, 16 };
|
|
+ case PixelFormat::GBGR422:
|
|
+ case PixelFormat::BGRG422:
|
|
+ return { 2, 1, 4 };
|
|
+ case PixelFormat::Depth16Unorm:
|
|
+ return { 1, 1, 2 };
|
|
+ case PixelFormat::Depth32Float:
|
|
+ return { 1, 1, 4 };
|
|
+ case PixelFormat::Stencil8:
|
|
+ case PixelFormat::X32_Stencil8:
|
|
+ case PixelFormat::X24_Stencil8:
|
|
+ return { 1, 1, 1 };
|
|
+ case PixelFormat::ASTC_4x4_sRGB:
|
|
+ case PixelFormat::ASTC_4x4_LDR:
|
|
+ case PixelFormat::ASTC_4x4_HDR:
|
|
+ return { 4, 4, 16 };
|
|
+ case PixelFormat::ASTC_5x4_sRGB:
|
|
+ case PixelFormat::ASTC_5x4_LDR:
|
|
+ case PixelFormat::ASTC_5x4_HDR:
|
|
+ return { 5, 4, 16 };
|
|
+ case PixelFormat::ASTC_5x5_sRGB:
|
|
+ case PixelFormat::ASTC_5x5_LDR:
|
|
+ case PixelFormat::ASTC_5x5_HDR:
|
|
+ return { 5, 5, 16 };
|
|
+ case PixelFormat::ASTC_6x5_sRGB:
|
|
+ case PixelFormat::ASTC_6x5_LDR:
|
|
+ case PixelFormat::ASTC_6x5_HDR:
|
|
+ return { 6, 5, 16 };
|
|
+ case PixelFormat::ASTC_6x6_sRGB:
|
|
+ case PixelFormat::ASTC_6x6_LDR:
|
|
+ case PixelFormat::ASTC_6x6_HDR:
|
|
+ return { 6, 6, 16 };
|
|
+ case PixelFormat::ASTC_8x5_sRGB:
|
|
+ case PixelFormat::ASTC_8x5_LDR:
|
|
+ case PixelFormat::ASTC_8x5_HDR:
|
|
+ return { 8, 5, 16 };
|
|
+ case PixelFormat::ASTC_8x6_sRGB:
|
|
+ case PixelFormat::ASTC_8x6_LDR:
|
|
+ case PixelFormat::ASTC_8x6_HDR:
|
|
+ return { 8, 6, 16 };
|
|
+ case PixelFormat::ASTC_8x8_sRGB:
|
|
+ case PixelFormat::ASTC_8x8_LDR:
|
|
+ case PixelFormat::ASTC_8x8_HDR:
|
|
+ return { 8, 8, 16 };
|
|
+ case PixelFormat::ASTC_10x5_sRGB:
|
|
+ case PixelFormat::ASTC_10x5_LDR:
|
|
+ case PixelFormat::ASTC_10x5_HDR:
|
|
+ return { 10, 5, 16 };
|
|
+ case PixelFormat::ASTC_10x6_sRGB:
|
|
+ case PixelFormat::ASTC_10x6_LDR:
|
|
+ case PixelFormat::ASTC_10x6_HDR:
|
|
+ return { 10, 6, 16 };
|
|
+ case PixelFormat::ASTC_10x8_sRGB:
|
|
+ case PixelFormat::ASTC_10x8_LDR:
|
|
+ case PixelFormat::ASTC_10x8_HDR:
|
|
+ return { 10, 8, 16 };
|
|
+ case PixelFormat::ASTC_10x10_sRGB:
|
|
+ case PixelFormat::ASTC_10x10_LDR:
|
|
+ case PixelFormat::ASTC_10x10_HDR:
|
|
+ return { 10, 10, 16 };
|
|
+ case PixelFormat::ASTC_12x10_sRGB:
|
|
+ case PixelFormat::ASTC_12x10_LDR:
|
|
+ case PixelFormat::ASTC_12x10_HDR:
|
|
+ return { 12, 10, 16 };
|
|
+ case PixelFormat::ASTC_12x12_sRGB:
|
|
+ case PixelFormat::ASTC_12x12_LDR:
|
|
+ case PixelFormat::ASTC_12x12_HDR:
|
|
+ return { 12, 12, 16 };
|
|
+ default: return { 1, 1, pixelFormatToByteCount(format) };
|
|
+ }
|
|
+ }
|
|
+
|
|
+ struct BufferImagePitch { uint32_t rowLength, imageHeight; size_t rowBytes, imageBytes; };
|
|
+
|
|
+ static inline BufferImagePitch bufferImagePitch(PixelFormat format, size_t width, size_t height,
|
|
+ size_t bytesPerRow, size_t bytesPerImage) {
|
|
+ auto block = pixelFormatTransferBlock(format);
|
|
+ if (!block.bytes) throw std::runtime_error("Unsupported pixel format for buffer/image transfer");
|
|
+ size_t tightRow = ((width + block.width - 1) / block.width) * block.bytes;
|
|
+ size_t rows = (height + block.height - 1) / block.height;
|
|
+ size_t row = bytesPerRow ? bytesPerRow : tightRow;
|
|
+ if (!row || row < tightRow || row % block.bytes ||
|
|
+ (bytesPerImage && (bytesPerImage % row || bytesPerImage / row < rows))) {
|
|
+ throw std::runtime_error("Invalid buffer/image transfer pitch");
|
|
+ }
|
|
+ return { static_cast<uint32_t>(bytesPerRow ? row / block.bytes * block.width : 0),
|
|
+ static_cast<uint32_t>(bytesPerImage ? bytesPerImage / row * block.height : 0),
|
|
+ row, bytesPerImage ? bytesPerImage : row * rows };
|
|
+ }
|
|
+
|
|
static constexpr bool pixelFormatIsCompressed(PixelFormat pixelFormat) {
|
|
switch (pixelFormat) {
|
|
case PixelFormat::R8Unorm:
|
|
@@ -716,8 +838,8 @@ namespace Indium {
|
|
case VertexFormat::UInt2: return VK_FORMAT_R32G32_UINT;
|
|
case VertexFormat::UInt3: return VK_FORMAT_R32G32B32_UINT;
|
|
case VertexFormat::UInt4: return VK_FORMAT_R32G32B32A32_UINT;
|
|
- case VertexFormat::Int1010102Normalized: return VK_FORMAT_A2R10G10B10_SINT_PACK32;
|
|
- case VertexFormat::UInt1010102Normalized: return VK_FORMAT_A2R10G10B10_UINT_PACK32;
|
|
+ case VertexFormat::Int1010102Normalized: return VK_FORMAT_A2B10G10R10_SNORM_PACK32;
|
|
+ case VertexFormat::UInt1010102Normalized: return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
|
case VertexFormat::UChar4Normalized_BGRA: return VK_FORMAT_B8G8R8A8_UNORM;
|
|
case VertexFormat::UChar: return VK_FORMAT_R8_UINT;
|
|
case VertexFormat::Char: return VK_FORMAT_R8_SINT;
|
|
diff --git a/private-include/iridium/dynamic-llvm.hpp b/private-include/iridium/dynamic-llvm.hpp
|
|
index b3756ef..ceb41ce 100644
|
|
--- a/private-include/iridium/dynamic-llvm.hpp
|
|
+++ b/private-include/iridium/dynamic-llvm.hpp
|
|
@@ -2,10 +2,14 @@
|
|
|
|
#include <llvm-c/BitReader.h>
|
|
#include <llvm-c/Core.h>
|
|
+#include <llvm-c/Transforms/PassBuilder.h>
|
|
+#include <llvm-c/Error.h>
|
|
|
|
#include <dlfcn.h>
|
|
|
|
#include <mutex>
|
|
+#include <string>
|
|
+#include <stdexcept>
|
|
|
|
namespace Iridium {
|
|
namespace DynamicLLVM {
|
|
@@ -58,6 +62,9 @@ namespace Iridium {
|
|
};
|
|
|
|
#define IRIDIUM_DYNAMICLLVM_FUNCTION_FOREACH(_macro) \
|
|
+ _macro(LLVMGlobalGetValueType) \
|
|
+ _macro(LLVMGetGEPSourceElementType) \
|
|
+ _macro(LLVMIsAPoisonValue) \
|
|
_macro(LLVMBasicBlockAsValue) \
|
|
_macro(LLVMConstIntGetSExtValue) \
|
|
_macro(LLVMConstIntGetZExtValue) \
|
|
@@ -68,6 +75,15 @@ namespace Iridium {
|
|
_macro(LLVMCreateMemoryBufferWithMemoryRange) \
|
|
_macro(LLVMDisposeMemoryBuffer) \
|
|
_macro(LLVMDisposeMessage) \
|
|
+ _macro(LLVMRunPasses) \
|
|
+ _macro(LLVMCreatePassBuilderOptions) \
|
|
+ _macro(LLVMDisposePassBuilderOptions) \
|
|
+ _macro(LLVMGetErrorMessage) \
|
|
+ _macro(LLVMDisposeErrorMessage) \
|
|
+ _macro(LLVMGetTarget) \
|
|
+ _macro(LLVMSetTarget) \
|
|
+ _macro(LLVMIsNull) \
|
|
+ _macro(LLVMPrintValueToString) \
|
|
_macro(LLVMDisposeModule) \
|
|
_macro(LLVMGetAlignment) \
|
|
_macro(LLVMGetArrayLength) \
|
|
@@ -77,6 +93,11 @@ namespace Iridium {
|
|
_macro(LLVMGetElementAsConstant) \
|
|
_macro(LLVMGetElementType) \
|
|
_macro(LLVMGetFCmpPredicate) \
|
|
+ _macro(LLVMGetICmpPredicate) \
|
|
+ _macro(LLVMInt32Type) \
|
|
+ _macro(LLVMGetFirstUse) \
|
|
+ _macro(LLVMGetNextUse) \
|
|
+ _macro(LLVMGetUser) \
|
|
_macro(LLVMGetFirstBasicBlock) \
|
|
_macro(LLVMGetFirstInstruction) \
|
|
_macro(LLVMGetFirstNamedMetadata) \
|
|
@@ -84,6 +105,7 @@ namespace Iridium {
|
|
_macro(LLVMGetIncomingValue) \
|
|
_macro(LLVMGetIndices) \
|
|
_macro(LLVMGetInitializer) \
|
|
+ _macro(LLVMGetAllocatedType) \
|
|
_macro(LLVMGetInstructionOpcode) \
|
|
_macro(LLVMGetIntTypeWidth) \
|
|
_macro(LLVMGetLastInstruction) \
|
|
@@ -107,6 +129,7 @@ namespace Iridium {
|
|
_macro(LLVMGetPointerAddressSpace) \
|
|
_macro(LLVMGetReturnType) \
|
|
_macro(LLVMGetSuccessor) \
|
|
+ _macro(LLVMGetNumSuccessors) \
|
|
_macro(LLVMGetTypeKind) \
|
|
_macro(LLVMGetUndefMaskElem) \
|
|
_macro(LLVMGetValueKind) \
|
|
diff --git a/private-include/iridium/spirv.hpp b/private-include/iridium/spirv.hpp
|
|
index 95cd3de..8fa6b1a 100644
|
|
--- a/private-include/iridium/spirv.hpp
|
|
+++ b/private-include/iridium/spirv.hpp
|
|
@@ -68,6 +68,7 @@ namespace Iridium {
|
|
Input = 1,
|
|
Uniform = 2,
|
|
Output = 3,
|
|
+ Private = 6,
|
|
Function = 7,
|
|
StorageBuffer = 12,
|
|
PhysicalStorageBuffer = 5349,
|
|
@@ -82,6 +83,7 @@ namespace Iridium {
|
|
Int64 = 11,
|
|
Int16 = 22,
|
|
Int8 = 39,
|
|
+ ImageQuery = 50,
|
|
PhysicalStorageBufferAddresses = 5347,
|
|
};
|
|
|
|
@@ -255,6 +257,10 @@ namespace Iridium {
|
|
ImageFormat imageFormat = ImageFormat::Unknown;
|
|
ResultID imageRealSampleTypeID = ResultIDInvalid;
|
|
|
|
+ // Types of Private/Function memory must not carry the explicit layout (Offset/ArrayStride)
|
|
+ // that buffer types need, so those are separate types. This IS part of the type's identity.
|
|
+ bool layoutFree = false;
|
|
+
|
|
// this does NOT need to be considered for equality
|
|
ResultID arrayLengthID = ResultIDInvalid;
|
|
|
|
@@ -372,6 +378,7 @@ struct std::hash<Iridium::SPIRV::Type> {
|
|
result = ((result << 1) ^ std::hash<size_t>()(type.scalarWidth)) >> 1;
|
|
result = ((result << 1) ^ std::hash<bool>()(type.integerIsSigned)) >> 1;
|
|
result = ((result << 1) ^ std::hash<size_t>()(type.entryCount)) >> 1;
|
|
+ result = ((result << 1) ^ std::hash<bool>()(type.layoutFree)) >> 1;
|
|
for (const auto& member: type.structureMembers) {
|
|
result = ((result << 1) ^ std::hash<Iridium::SPIRV::ResultID>()(member.id)) >> 1;
|
|
for (const auto& decoration: member.decorations) {
|
|
@@ -412,6 +419,7 @@ namespace Iridium {
|
|
struct GlobalVariable {
|
|
StorageClass storageClass;
|
|
ResultID typeID;
|
|
+ ResultID initializer = ResultIDInvalid;
|
|
};
|
|
|
|
struct FunctionVariable {
|
|
@@ -485,7 +493,7 @@ namespace Iridium {
|
|
IMul = 132,
|
|
FMul = 133,
|
|
UDiv = 134,
|
|
- SDiv = 133,
|
|
+ SDiv = 135,
|
|
FDiv = 136,
|
|
UMod = 137,
|
|
SRem = 138,
|
|
@@ -618,12 +626,14 @@ namespace Iridium {
|
|
std::unordered_map<ResultID, DynamicByteWriter> _functionWriters;
|
|
PrivateFunctionInfo* _currentFunctionInfo = nullptr;
|
|
DynamicByteWriter* _currentFunctionWriter = nullptr;
|
|
+ std::vector<std::pair<size_t, uintptr_t>> _phiFixups;
|
|
std::unordered_set<Capability> _requiredCapabilities;
|
|
uint8_t _versionMajor = 1;
|
|
uint8_t _versionMinor = 0;
|
|
uint16_t _generatorID = 0;
|
|
uint16_t _generatorVersion = 0;
|
|
std::unordered_map<std::pair<uintmax_t, ResultID>, ResultID> _constantScalars;
|
|
+ std::unordered_map<std::pair<uintmax_t, ResultID>, ResultID> _constantBools;
|
|
std::unordered_map<ResultID, ResultID> _undefinedValues;
|
|
std::unordered_map<ResultID, ResultID> _nullValues;
|
|
std::unordered_map<ResultID, ResultID> _resultIDTypes;
|
|
@@ -673,7 +683,8 @@ namespace Iridium {
|
|
ResultID lookupResultID(uintptr_t association) const;
|
|
|
|
void addEntryPoint(const EntryPoint& entryPoint);
|
|
- ResultID addGlobalVariable(ResultID typeID, StorageClass storageClass);
|
|
+ ResultID addGlobalVariable(ResultID typeID, StorageClass storageClass, ResultID initializer = ResultIDInvalid);
|
|
+ ResultID addFunctionVariable(ResultID typeID);
|
|
void addDecoration(ResultID resultID, const Decoration& decoration);
|
|
FunctionInfo declareFunction(ResultID functionType);
|
|
|
|
@@ -686,6 +697,7 @@ namespace Iridium {
|
|
ResultID declareConstantComposite(ResultID typeID, std::vector<ResultID> elements);
|
|
|
|
ResultID declareUndefinedValue(ResultID typeID);
|
|
+ ResultID declareConstantBool(bool value);
|
|
ResultID declareNullValue(ResultID typeID);
|
|
|
|
ResultID insertLabel(ResultID id = ResultIDInvalid);
|
|
@@ -717,9 +729,13 @@ namespace Iridium {
|
|
ResultID encodeFOrdGreaterThan(ResultID operand1, ResultID operand2);
|
|
void encodeBranch(ResultID targetLabel);
|
|
void encodeBranchConditional(ResultID condition, ResultID trueLabel, ResultID falseLabel);
|
|
- ResultID encodePhi(ResultID resultTypeID, std::vector<std::pair<ResultID, ResultID>> variablesAndBlocks);
|
|
+ ResultID encodePhi(ResultID resultTypeID, std::vector<std::pair<ResultID, ResultID>> variablesAndBlocks, ResultID result = ResultIDInvalid, std::vector<uintptr_t> forwardAssociations = {});
|
|
void encodeSelectionMerge(ResultID mergeBlock);
|
|
ResultID encodeSqrt(ResultID resultTypeID, ResultID target);
|
|
+ // Generic emitters for instructions without a dedicated helper. The operands are result IDs.
|
|
+ ResultID encodeRawOp(uint16_t opcode, ResultID resultTypeID, const std::vector<ResultID>& operands);
|
|
+ void encodeRawOpNoResult(uint16_t opcode, const std::vector<ResultID>& operands);
|
|
+ ResultID encodeGLSLOp(GLSLOpcode opcode, ResultID resultTypeID, const std::vector<ResultID>& operands);
|
|
ResultID encodeTrunc(ResultID resultTypeID, ResultID operand);
|
|
|
|
void encodeDebugPrint(std::string format, std::vector<ResultID> arguments);
|
|
diff --git a/src/indium-kit/layer.cpp b/src/indium-kit/layer.cpp
|
|
index 8d00405..6a5fde8 100644
|
|
--- a/src/indium-kit/layer.cpp
|
|
+++ b/src/indium-kit/layer.cpp
|
|
@@ -105,6 +105,7 @@ void IndiumKit::PrivateDrawable::present() {
|
|
info.pImageIndices = &_swapchainImageIndex;
|
|
info.pResults = nullptr;
|
|
|
|
+ std::lock_guard<std::mutex> queueLock(privateDevice->graphicsQueueMutex());
|
|
if (DynamicVK::vkQueuePresentKHR(privateDevice->graphicsQueue(), &info) != VK_SUCCESS) {
|
|
// TODO
|
|
abort();
|
|
diff --git a/src/indium/blit-command-encoder.cpp b/src/indium/blit-command-encoder.cpp
|
|
index e58f0ba..616d177 100644
|
|
--- a/src/indium/blit-command-encoder.cpp
|
|
+++ b/src/indium/blit-command-encoder.cpp
|
|
@@ -3,6 +3,7 @@
|
|
#include <indium/texture.private.hpp>
|
|
#include <indium/buffer.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/command-encoder.private.hpp>
|
|
|
|
Indium::BlitCommandEncoder::~BlitCommandEncoder() {};
|
|
|
|
@@ -17,14 +18,27 @@ Indium::PrivateBlitCommandEncoder::PrivateBlitCommandEncoder(std::shared_ptr<Pri
|
|
_descriptor(descriptor)
|
|
{};
|
|
|
|
+void Indium::PrivateBlitCommandEncoder::noteDestination(const std::shared_ptr<Texture>& destination) {
|
|
+ // Drawables are presented by another API, so writes to them must signal a presentation semaphore like render passes do
|
|
+ auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
|
+ if (privateTexture && privateTexture->needsExportablePresentationSemaphore()) {
|
|
+ for (const auto& known: _readWriteTextures) {
|
|
+ if (known == destination) {
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ _readWriteTextures.push_back(destination);
|
|
+ }
|
|
+};
|
|
+
|
|
Indium::PrivateBlitCommandEncoder::~PrivateBlitCommandEncoder() {
|
|
// nothing for now
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) {
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
|
- auto privateDest = std::dynamic_pointer_cast<PrivateBuffer>(destination);
|
|
+ auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
|
+ auto* privateDest = dynamic_cast<PrivateBuffer*>(destination.get());
|
|
|
|
VkBufferCopy info {};
|
|
|
|
@@ -37,18 +51,18 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
|
DynamicVK::vkCmdCopyBuffer(cmdbuf->commandBuffer(), privateSource->buffer(), privateDest->buffer(), 1, &info);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options) {
|
|
+ noteDestination(destination);
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateSource = std::dynamic_pointer_cast<PrivateBuffer>(source);
|
|
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
|
+ auto* privateSource = dynamic_cast<PrivateBuffer*>(source.get());
|
|
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
|
|
|
if (options != BlitOption::None) {
|
|
throw std::runtime_error("TODO: support blit options");
|
|
}
|
|
|
|
auto aspect = pixelFormatToVkImageAspectFlags(privateDest->pixelFormat());
|
|
- // FIXME: handle compressed formats
|
|
- size_t bytesPerPixel = pixelFormatToByteCount(privateDest->pixelFormat());
|
|
+ auto pitch = bufferImagePitch(privateDest->pixelFormat(), sourceSize.width, sourceSize.height, sourceBytesPerRow, sourceBytesPerImage);
|
|
|
|
// first, transition the image to the optimal layout for transfer destinations
|
|
VkImageMemoryBarrier barrier {};
|
|
@@ -71,9 +85,9 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
|
|
|
// now encode the copy
|
|
VkBufferImageCopy copyInfo {};
|
|
- copyInfo.bufferOffset = 0;
|
|
- copyInfo.bufferRowLength = sourceBytesPerRow / bytesPerPixel;
|
|
- copyInfo.bufferImageHeight = sourceBytesPerImage / sourceBytesPerRow;
|
|
+ copyInfo.bufferOffset = sourceOffset;
|
|
+ copyInfo.bufferRowLength = pitch.rowLength;
|
|
+ copyInfo.bufferImageHeight = pitch.imageHeight;
|
|
copyInfo.imageSubresource.aspectMask = aspect;
|
|
copyInfo.imageSubresource.mipLevel = destinationLevel;
|
|
copyInfo.imageSubresource.baseArrayLayer = destinationSlice;
|
|
@@ -95,18 +109,17 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Buffer> source, siz
|
|
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options) {
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
|
- auto privateDest = std::dynamic_pointer_cast<PrivateBuffer>(destination);
|
|
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
|
+ auto* privateDest = dynamic_cast<PrivateBuffer*>(destination.get());
|
|
|
|
if (options != BlitOption::None) {
|
|
throw std::runtime_error("TODO: support blit options");
|
|
}
|
|
|
|
auto aspect = pixelFormatToVkImageAspectFlags(privateSource->pixelFormat());
|
|
- // FIXME: handle compressed formats
|
|
- size_t bytesPerPixel = pixelFormatToByteCount(privateSource->pixelFormat());
|
|
+ auto pitch = bufferImagePitch(privateSource->pixelFormat(), sourceSize.width, sourceSize.height, destinationBytesPerRow, destinationBytesPerImage);
|
|
|
|
// first, transition the image to the optimal layout for transfer sources
|
|
VkImageMemoryBarrier barrier {};
|
|
@@ -129,9 +142,9 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
|
|
|
// now encode the copy
|
|
VkBufferImageCopy copyInfo {};
|
|
- copyInfo.bufferOffset = 0;
|
|
- copyInfo.bufferRowLength = destinationBytesPerRow / bytesPerPixel;
|
|
- copyInfo.bufferImageHeight = destinationBytesPerImage / destinationBytesPerRow;
|
|
+ copyInfo.bufferOffset = destinationOffset;
|
|
+ copyInfo.bufferRowLength = pitch.rowLength;
|
|
+ copyInfo.bufferImageHeight = pitch.imageHeight;
|
|
copyInfo.imageSubresource.aspectMask = aspect;
|
|
copyInfo.imageSubresource.mipLevel = sourceLevel;
|
|
copyInfo.imageSubresource.baseArrayLayer = sourceSlice;
|
|
@@ -153,10 +166,11 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
|
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, size_t sliceCount, size_t levelCount, Size size) {
|
|
+ noteDestination(destination);
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
|
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
|
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
|
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
|
|
|
auto aspect = pixelFormatToVkImageAspectFlags(privateSource->pixelFormat());
|
|
|
|
@@ -231,10 +245,10 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, si
|
|
DynamicVK::vkCmdPipelineBarrier(cmdbuf->commandBuffer(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, sizeof(barriers) / sizeof(*barriers), barriers);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) {
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateSource = std::dynamic_pointer_cast<PrivateTexture>(source);
|
|
- auto privateDest = std::dynamic_pointer_cast<PrivateTexture>(destination);
|
|
+ auto* privateSource = dynamic_cast<PrivateTexture*>(source.get());
|
|
+ auto* privateDest = dynamic_cast<PrivateTexture*>(destination.get());
|
|
size_t sourceMipLevel = SIZE_MAX;
|
|
size_t destMipLevel = SIZE_MAX;
|
|
|
|
@@ -282,18 +296,18 @@ void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, st
|
|
copy(source, 0, sourceMipLevel, Origin { 0, 0, 0 }, destination, 0, destMipLevel, Origin { 0, 0, 0 }, sliceCount, mipCount, Size { source->width() / divisor, source->height() / divisor, source->depth() / divisor });
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) {
|
|
size_t divisor = static_cast<size_t>(1) << sourceLevel;
|
|
copy(source, sourceSlice, sourceLevel, Origin { 0, 0, 0 }, destination, destinationSlice, destinationLevel, Origin { 0, 0, 0 }, sliceCount, levelCount, Size { source->width() / divisor, source->height() / divisor, source->depth() / divisor });
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) {
|
|
+void Indium::PrivateBlitCommandEncoder::copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) {
|
|
copy(source, sourceSlice, sourceLevel, sourceOrigin, destination, destinationSlice, destinationLevel, destinationOrigin, 1, 1, sourceSize);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) {
|
|
+void Indium::PrivateBlitCommandEncoder::fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) {
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
- auto privateBuffer = std::dynamic_pointer_cast<PrivateBuffer>(buffer);
|
|
+ auto* privateBuffer = dynamic_cast<PrivateBuffer*>(buffer.get());
|
|
uint32_t value32 =
|
|
((uint32_t)value << 24) |
|
|
((uint32_t)value << 16) |
|
|
@@ -314,8 +328,9 @@ void Indium::PrivateBlitCommandEncoder::fillBuffer(std::shared_ptr<Buffer> buffe
|
|
DynamicVK::vkCmdFillBuffer(cmdbuf->commandBuffer(), privateBuffer->buffer(), range.start, range.length, value32);
|
|
};
|
|
|
|
-void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_ptr<Texture> texture) {
|
|
- auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
|
+void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) {
|
|
+ noteDestination(texture);
|
|
+ auto* privateTexture = dynamic_cast<PrivateTexture*>(texture.get());
|
|
auto cmdbuf = _privateCommandBuffer.lock();
|
|
|
|
auto aspect = pixelFormatToVkImageAspectFlags(privateTexture->pixelFormat());
|
|
@@ -439,5 +454,5 @@ void Indium::PrivateBlitCommandEncoder::generateMipmapsForTexture(std::shared_pt
|
|
};
|
|
|
|
void Indium::PrivateBlitCommandEncoder::endEncoding() {
|
|
- // nothing for now
|
|
+ finishTrackedEncoder(_privateCommandBuffer.lock()->commandBuffer());
|
|
};
|
|
diff --git a/src/indium/buffer.cpp b/src/indium/buffer.cpp
|
|
index 23fd2d3..1a48b81 100644
|
|
--- a/src/indium/buffer.cpp
|
|
+++ b/src/indium/buffer.cpp
|
|
@@ -1,6 +1,8 @@
|
|
#include <indium/buffer.private.hpp>
|
|
#include <indium/device.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/counters.private.hpp>
|
|
+#include <indium/memory-pool.private.hpp>
|
|
|
|
#include <cstring>
|
|
|
|
@@ -43,49 +45,17 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr<PrivateDevice> device, size
|
|
|
|
DynamicVK::vkGetBufferMemoryRequirements(_privateDevice->device(), _buffer, &requirements);
|
|
|
|
- size_t targetIndex = SIZE_MAX;
|
|
-
|
|
- for (size_t i = 0; i < _privateDevice->memoryProperties().memoryTypeCount; ++i) {
|
|
- const auto& type = _privateDevice->memoryProperties().memoryTypes[i];
|
|
-
|
|
- if ((requirements.memoryTypeBits & (1 << i)) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- if ((_storageMode == StorageMode::Managed || _storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- if (_storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- // okay, this is good enough
|
|
- targetIndex = i;
|
|
- break;
|
|
- }
|
|
+ size_t targetIndex = _privateDevice->findMemoryType(requirements.memoryTypeBits, _storageMode, false);
|
|
|
|
if (targetIndex == SIZE_MAX) {
|
|
throw std::runtime_error("No suitable memory region found for buffer with requested storage mode");
|
|
}
|
|
|
|
- VkMemoryAllocateInfo allocateInfo {};
|
|
- allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
- allocateInfo.allocationSize = requirements.size;
|
|
- allocateInfo.memoryTypeIndex = targetIndex;
|
|
+ Indium::count(length < 4096 ? "bufferAlloc<4K" : (length < 65536 ? "bufferAlloc<64K" : (length < 4194304 ? "bufferAlloc<4M" : "bufferAlloc>=4M")), length);
|
|
+ auto allocation = MemoryPool::allocate(_privateDevice->device(), reinterpret_cast<uint64_t>(_buffer), targetIndex, requirements.size, requirements.alignment, false, true);
|
|
+ _memory = allocation.memory;
|
|
|
|
- VkMemoryAllocateFlagsInfo allocateFlags {};
|
|
- allocateFlags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO;
|
|
- allocateFlags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT;
|
|
-
|
|
- allocateInfo.pNext = &allocateFlags;
|
|
-
|
|
- if (DynamicVK::vkAllocateMemory(_privateDevice->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
-
|
|
- DynamicVK::vkBindBufferMemory(_privateDevice->device(), _buffer, _memory, 0);
|
|
+ DynamicVK::vkBindBufferMemory(_privateDevice->device(), _buffer, _memory, allocation.offset);
|
|
};
|
|
|
|
Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr<PrivateDevice> device, const void* pointer, size_t length, ResourceOptions options):
|
|
@@ -103,9 +73,11 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr<PrivateDevice> device, cons
|
|
if (_storageMode == StorageMode::Managed) {
|
|
VkMappedMemoryRange range {};
|
|
range.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
|
|
+ MemoryPool::Allocation allocation;
|
|
+ MemoryPool::lookup(reinterpret_cast<uint64_t>(_buffer), allocation);
|
|
range.memory = _memory;
|
|
- range.size = VK_WHOLE_SIZE;
|
|
- range.offset = 0;
|
|
+ range.size = allocation.size;
|
|
+ range.offset = allocation.offset;
|
|
if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &range) != VK_SUCCESS) {
|
|
// TODO
|
|
abort();
|
|
@@ -114,12 +86,10 @@ Indium::PrivateBuffer::PrivateBuffer(std::shared_ptr<PrivateDevice> device, cons
|
|
};
|
|
|
|
Indium::PrivateBuffer::~PrivateBuffer() {
|
|
- if (_mapped) {
|
|
- DynamicVK::vkUnmapMemory(_privateDevice->device(), _memory);
|
|
- }
|
|
-
|
|
+ // the mapping belongs to the shared memory block
|
|
+ auto owner = reinterpret_cast<uint64_t>(_buffer);
|
|
DynamicVK::vkDestroyBuffer(_privateDevice->device(), _buffer, nullptr);
|
|
- DynamicVK::vkFreeMemory(_privateDevice->device(), _memory, nullptr);
|
|
+ MemoryPool::free(_privateDevice->device(), owner);
|
|
};
|
|
|
|
std::shared_ptr<Indium::Device> Indium::PrivateBuffer::device() {
|
|
@@ -136,7 +106,8 @@ void* Indium::PrivateBuffer::contents() {
|
|
}
|
|
|
|
if (!_mapped) {
|
|
- if (DynamicVK::vkMapMemory(_privateDevice->device(), _memory, 0, VK_WHOLE_SIZE, 0, &_mapped) != VK_SUCCESS) {
|
|
+ _mapped = MemoryPool::map(_privateDevice->device(), reinterpret_cast<uint64_t>(_buffer));
|
|
+ if (!_mapped) {
|
|
// TODO
|
|
abort();
|
|
}
|
|
@@ -148,9 +119,16 @@ void* Indium::PrivateBuffer::contents() {
|
|
void Indium::PrivateBuffer::didModifyRange(Range<size_t> range) {
|
|
VkMappedMemoryRange vulkanRange {};
|
|
vulkanRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE;
|
|
+ // flush whole 256-byte granules of this buffer's part of the block (granule >= nonCoherentAtomSize)
|
|
+ MemoryPool::Allocation allocation;
|
|
+ MemoryPool::lookup(reinterpret_cast<uint64_t>(_buffer), allocation);
|
|
+ VkDeviceSize first = range.start / 256 * 256;
|
|
+ VkDeviceSize last = (range.start + range.length + 255) / 256 * 256;
|
|
+ if (last > allocation.size) last = allocation.size;
|
|
+ if (first >= last) return;
|
|
vulkanRange.memory = _memory;
|
|
- vulkanRange.size = range.length;
|
|
- vulkanRange.offset = range.start;
|
|
+ vulkanRange.size = last - first;
|
|
+ vulkanRange.offset = allocation.offset + first;
|
|
if (DynamicVK::vkFlushMappedMemoryRanges(_privateDevice->device(), 1, &vulkanRange) != VK_SUCCESS) {
|
|
// TODO
|
|
abort();
|
|
@@ -158,8 +136,10 @@ void Indium::PrivateBuffer::didModifyRange(Range<size_t> range) {
|
|
};
|
|
|
|
uint64_t Indium::PrivateBuffer::gpuAddress() {
|
|
+ if (_gpuAddress) return _gpuAddress;
|
|
VkBufferDeviceAddressInfo info {};
|
|
info.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO;
|
|
info.buffer = _buffer;
|
|
- return DynamicVK::vkGetBufferDeviceAddress(_privateDevice->device(), &info);
|
|
+ _gpuAddress = DynamicVK::vkGetBufferDeviceAddress(_privateDevice->device(), &info);
|
|
+ return _gpuAddress;
|
|
};
|
|
diff --git a/src/indium/command-buffer.cpp b/src/indium/command-buffer.cpp
|
|
index 43f4a84..46a4f72 100644
|
|
--- a/src/indium/command-buffer.cpp
|
|
+++ b/src/indium/command-buffer.cpp
|
|
@@ -1,3 +1,8 @@
|
|
+#include <indium/submit-thread.private.hpp>
|
|
+#include <indium/gpu-timing.private.hpp>
|
|
+#include <indium/submit-batch.private.hpp>
|
|
+#include <indium/deferred-render-encoder.private.hpp>
|
|
+#include <indium/deferred-blit-encoder.private.hpp>
|
|
#include <indium/command-buffer.private.hpp>
|
|
#include <indium/command-queue.private.hpp>
|
|
#include <indium/device.private.hpp>
|
|
@@ -8,7 +13,116 @@
|
|
#include <indium/compute-command-encoder.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
|
|
+#include <algorithm>
|
|
+#include <array>
|
|
+#include <atomic>
|
|
+#include <chrono>
|
|
+#include <cstdio>
|
|
+#include <map>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+#include <unistd.h>
|
|
+#include <indium/counters.private.hpp>
|
|
#include <condition_variable>
|
|
+#include <indium/retirement-queue.private.hpp>
|
|
+
|
|
+// Aggregated timings, printed every 5 s, when /tmp/metal-debug-timing exists in the guest at process start
|
|
+static bool indiumTimingEnabled() {
|
|
+ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0;
|
|
+ return enabled;
|
|
+}
|
|
+static double indiumNow() {
|
|
+ return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
+}
|
|
+static void indiumTiming(const char* what, double seconds) {
|
|
+ struct Stat { double sum = 0, max = 0; unsigned long count = 0; };
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, Stat> stats;
|
|
+ static double last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ Stat& stat = stats[what];
|
|
+ stat.sum += seconds;
|
|
+ stat.count++;
|
|
+ if (seconds > stat.max) stat.max = seconds;
|
|
+ double now = indiumNow();
|
|
+ if (now - last >= 5) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "indium-timing@%.0f:", now);
|
|
+ for (auto& entry: stats) {
|
|
+ fprintf(log, " %s[n=%lu avg=%.1fms max=%.1fms]", entry.first.c_str(), entry.second.count, entry.second.sum / entry.second.count * 1000, entry.second.max * 1000);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ stats.clear();
|
|
+ }
|
|
+}
|
|
+
|
|
+// Opt-in, bounded capture: no per-frame file I/O, no draw logging.
|
|
+// Timestamps describe presenting command-buffer commits, not compositor scanout.
|
|
+namespace {
|
|
+struct FrameIntervalSample {
|
|
+ std::atomic<int64_t> commit{0}, submitted{0}, presented{0}, completed{0};
|
|
+};
|
|
+static int64_t frameIntervalNow() {
|
|
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
+}
|
|
+static FrameIntervalSample* beginFrameIntervalSample(bool presenting) {
|
|
+ if (!presenting) return nullptr;
|
|
+ static const int64_t start = frameIntervalNow();
|
|
+ static std::atomic<bool> enabled {false};
|
|
+ static std::atomic<size_t> checks {0};
|
|
+ // Late activation permits profiling the existing match without another login.
|
|
+ // Only one file check per 128 presenting frames until activated; none after.
|
|
+ if (!enabled.load(std::memory_order_relaxed) &&
|
|
+ (checks.fetch_add(1, std::memory_order_relaxed) % 128) == 0) {
|
|
+ if (FILE* flag = fopen("/tmp/metal-frame-interval-capture", "r")) {
|
|
+ fclose(flag);
|
|
+ enabled.store(true, std::memory_order_relaxed);
|
|
+ }
|
|
+ }
|
|
+ if (!enabled.load(std::memory_order_relaxed)) return nullptr;
|
|
+ const int64_t now = frameIntervalNow();
|
|
+ static const bool testing = getenv("RIOT_FRAME_CAPTURE_TEST") != nullptr;
|
|
+ if (!testing && now - start < 90000000000LL) return nullptr; // discard reconnect/warmup
|
|
+ static std::array<FrameIntervalSample, 4096> samples;
|
|
+ static std::atomic<size_t> count{0};
|
|
+ static std::atomic<bool> dumped{false};
|
|
+ static const size_t limit = testing ? 8 : samples.size();
|
|
+ const size_t index = count.fetch_add(1, std::memory_order_relaxed);
|
|
+ if (index < limit) {
|
|
+ samples[index].commit.store(now, std::memory_order_relaxed);
|
|
+ return &samples[index];
|
|
+ }
|
|
+ if (dumped.load(std::memory_order_relaxed)) return nullptr;
|
|
+
|
|
+ if (dumped.exchange(true)) return nullptr;
|
|
+ if (FILE* log = fopen("/tmp/metal-frame-intervals.csv", "w")) {
|
|
+ fprintf(log, "commit_ns,submitted_ns,presented_ns,completed_ns\n");
|
|
+ for (size_t i=0;i<limit;++i) { const auto& sample=samples[i]; fprintf(log, "%lld,%lld,%lld,%lld\n", (long long)sample.commit.load(), (long long)sample.submitted.load(), (long long)sample.presented.load(), (long long)sample.completed.load()); }
|
|
+ fclose(log);
|
|
+ }
|
|
+ return nullptr;
|
|
+}
|
|
+}
|
|
+
|
|
+namespace {
|
|
+// Submission counters for A/B runs: one line per 1024 presented frames in guest /tmp/indium-submit-stats.log.
|
|
+std::atomic<uint64_t> statCommits {0}, statSubmits {0}, statPresents {0};
|
|
+void submitStatsPresented() {
|
|
+ const uint64_t presents = statPresents.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
+ if (presents % 1024 != 0) return;
|
|
+ static uint64_t lastCommits = 0, lastSubmits = 0;
|
|
+ const uint64_t commits = statCommits.load(std::memory_order_relaxed), submits = statSubmits.load(std::memory_order_relaxed);
|
|
+ if (FILE* log = fopen("/tmp/indium-submit-stats.log", "a")) {
|
|
+ fprintf(log, "%lld batch=%d commits_per_frame=%.2f submits_per_frame=%.2f\n", (long long)frameIntervalNow(), (int)Indium::submitBatchEnabled(),
|
|
+ (commits - lastCommits) / 1024.0, (submits - lastSubmits) / 1024.0);
|
|
+ fclose(log);
|
|
+ }
|
|
+ lastCommits = commits; lastSubmits = submits;
|
|
+}
|
|
+}
|
|
|
|
Indium::CommandBuffer::~CommandBuffer() {};
|
|
|
|
@@ -19,18 +133,47 @@ std::shared_ptr<Indium::Device> Indium::PrivateCommandBuffer::device() {
|
|
return _privateDevice;
|
|
};
|
|
|
|
+namespace {
|
|
+// OFF by default (round-3 behaviour: the creating thread leases the Vulkan command buffer in the constructor). Guest
|
|
+// flag file /tmp/metal-worker-lease (checked once per process, so the constructor and every later recorder agree)
|
|
+// makes the lease lazy: the thread that first records leases it, which with deferred encoding is the submit worker.
|
|
+// Round 4 had this on unconditionally; that round regressed in matches, so it is an opt-in experiment now.
|
|
+bool workerLeaseEnabled() {
|
|
+ static const bool value = access("/tmp/metal-worker-lease", F_OK) == 0;
|
|
+ return value;
|
|
+}
|
|
+}
|
|
+
|
|
Indium::PrivateCommandBuffer::PrivateCommandBuffer(std::shared_ptr<PrivateCommandQueue> commandQueue):
|
|
_privateCommandQueue(commandQueue)
|
|
{
|
|
_privateDevice = _privateCommandQueue->privateDevice();
|
|
+ // Default: lease now, on the creating thread. With /tmp/metal-worker-lease the Vulkan command buffer (pool reset,
|
|
+ // allocation, begin) is leased lazily by the thread that first records into it (see ensureRecordingStarted()).
|
|
+ if (!workerLeaseEnabled()) leaseRecording();
|
|
+};
|
|
+
|
|
+// Only called with /tmp/metal-worker-lease (callers test workerLeaseEnabled() first; without the flag the
|
|
+// constructor has already leased).
|
|
+void Indium::PrivateCommandBuffer::ensureRecordingStarted() {
|
|
+ std::scoped_lock lock(_mutex);
|
|
+ if (_commandBuffer) return;
|
|
+ leaseRecording();
|
|
+};
|
|
+
|
|
+// The lease itself; runs in the constructor or under _mutex in ensureRecordingStarted().
|
|
+void Indium::PrivateCommandBuffer::leaseRecording() {
|
|
+ // A pool is exclusively leased through recording and GPU completion.
|
|
+ // Freeing another buffer from the same pool can otherwise race recording.
|
|
+ _recordingPool = _privateCommandQueue->acquireCommandPool(_commandBuffer);
|
|
|
|
VkCommandBufferAllocateInfo allocInfo {};
|
|
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
- allocInfo.commandPool = _privateCommandQueue->commandPool();
|
|
+ allocInfo.commandPool = _recordingPool;
|
|
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
allocInfo.commandBufferCount = 1;
|
|
|
|
- if (DynamicVK::vkAllocateCommandBuffers(_privateDevice->device(), &allocInfo, &_commandBuffer) != VK_SUCCESS) {
|
|
+ if (!_commandBuffer && DynamicVK::vkAllocateCommandBuffers(_privateDevice->device(), &allocInfo, &_commandBuffer) != VK_SUCCESS) {
|
|
// TODO: handle this in a more C++-friendly way
|
|
abort();
|
|
}
|
|
@@ -41,13 +184,64 @@ Indium::PrivateCommandBuffer::PrivateCommandBuffer(std::shared_ptr<PrivateComman
|
|
// TODO: same
|
|
abort();
|
|
}
|
|
+ GpuTiming::beginSpan(_privateDevice->device(), _commandBuffer);
|
|
};
|
|
|
|
+namespace {
|
|
+struct EncoderRetirement {
|
|
+ std::vector<std::shared_ptr<Indium::CommandEncoder>> encoders;
|
|
+ std::shared_ptr<Indium::PrivateCommandQueue> queue;
|
|
+ VkCommandPool pool;
|
|
+ VkCommandBuffer buffer;
|
|
+ void operator()() {
|
|
+ encoders.clear();
|
|
+ // a command buffer that never recorded (created and dropped without an encoder) leased no pool
|
|
+ if (pool) queue->releaseCommandPool(pool, buffer);
|
|
+ queue.reset();
|
|
+ }
|
|
+};
|
|
+static Indium::RetirementQueue<EncoderRetirement>& encoderRetirementQueue() {
|
|
+ static Indium::RetirementQueue<EncoderRetirement> queue;
|
|
+ return queue;
|
|
+}
|
|
+}
|
|
+namespace Indium {
|
|
+void finishCommandRetirement() { encoderRetirementQueue().shutdown(); }
|
|
+}
|
|
+namespace {
|
|
+// Live experiment (guest flag, re-read every 256 command buffers like the async-encode flag):
|
|
+// /tmp/metal-retire-tiny makes the tiny buffers take the worker path too, i.e. the encoder
|
|
+// destructors and the command-pool release run on the retirement thread instead of the glyph
|
|
+// thread that released the MTLCommandBuffer. Only the *cost* is in question here - the handoff is
|
|
+// the same one heavy buffers already use, the queue's contract is completed GPU work (and an
|
|
+// unsubmitted buffer holds no GPU work), and a full queue still falls back to the inline release -
|
|
+// so the flag is the measurement, and the default stays the measured-cheaper inline release.
|
|
+bool retireTinyBuffers() {
|
|
+ static std::atomic<unsigned> calls { 0 };
|
|
+ static std::atomic<bool> value { false };
|
|
+ if ((calls.fetch_add(1, std::memory_order_relaxed) & 255) == 0)
|
|
+ value.store(access("/tmp/metal-retire-tiny", F_OK) == 0, std::memory_order_relaxed);
|
|
+ return value.load(std::memory_order_relaxed);
|
|
+}
|
|
+}
|
|
Indium::PrivateCommandBuffer::~PrivateCommandBuffer() {
|
|
- DynamicVK::vkFreeCommandBuffers(_privateDevice->device(), _privateCommandQueue->commandPool(), 1, &_commandBuffer);
|
|
+ // Submitted buffers are held by the completion callback until GPU work and
|
|
+ // user completion handlers finish. Unsubmitted buffers have no GPU users.
|
|
+ // Tiny buffers cost less to release directly than to signal a Darwin
|
|
+ // condition variable and wake another worker through Darling's server.
|
|
+ bool heavy = _commandEncoders.size() >= 4 || retireTinyBuffers();
|
|
+ if (!heavy) {
|
|
+ for (const auto& encoder : _commandEncoders) {
|
|
+ auto* render = dynamic_cast<PrivateRenderCommandEncoder*>(encoder.get());
|
|
+ if (render && render->hasHeavyRetirement()) { heavy = true; break; }
|
|
+ }
|
|
+ }
|
|
+ EncoderRetirement job {std::move(_commandEncoders), _privateCommandQueue, _recordingPool, _commandBuffer};
|
|
+ if (!heavy || !encoderRetirementQueue().tryPush(std::move(job))) job();
|
|
};
|
|
|
|
-std::shared_ptr<Indium::RenderCommandEncoder> Indium::PrivateCommandBuffer::renderCommandEncoder(const RenderPassDescriptor& descriptor) {
|
|
+std::shared_ptr<Indium::PrivateRenderCommandEncoder> Indium::PrivateCommandBuffer::renderCommandEncoderNow(const RenderPassDescriptor& descriptor) {
|
|
+ if (workerLeaseEnabled()) ensureRecordingStarted();
|
|
auto encoder = std::make_shared<Indium::PrivateRenderCommandEncoder>(shared_from_this(), descriptor);
|
|
{
|
|
std::scoped_lock lock(_mutex);
|
|
@@ -56,7 +250,65 @@ std::shared_ptr<Indium::RenderCommandEncoder> Indium::PrivateCommandBuffer::rend
|
|
return encoder;
|
|
};
|
|
|
|
+std::shared_ptr<Indium::PrivateBlitCommandEncoder> Indium::PrivateCommandBuffer::blitCommandEncoderNow() {
|
|
+ return blitCommandEncoderNow(BlitPassDescriptor {});
|
|
+};
|
|
+
|
|
+// Creates the real blit encoder on whichever thread replays the deferred op list (see deferred-blit-encoder.private.hpp).
|
|
+std::shared_ptr<Indium::PrivateBlitCommandEncoder> Indium::PrivateCommandBuffer::blitCommandEncoderNow(const BlitPassDescriptor& descriptor) {
|
|
+ if (workerLeaseEnabled()) ensureRecordingStarted();
|
|
+ auto encoder = std::make_shared<Indium::PrivateBlitCommandEncoder>(shared_from_this(), descriptor);
|
|
+ {
|
|
+ std::scoped_lock lock(_mutex);
|
|
+ _commandEncoders.push_back(encoder);
|
|
+ }
|
|
+ return encoder;
|
|
+};
|
|
+
|
|
+std::shared_ptr<Indium::PrivateComputeCommandEncoder> Indium::PrivateCommandBuffer::computeCommandEncoderNow(const ComputePassDescriptor& descriptor) {
|
|
+ if (workerLeaseEnabled()) ensureRecordingStarted();
|
|
+ auto encoder = std::make_shared<Indium::PrivateComputeCommandEncoder>(shared_from_this(), descriptor);
|
|
+ {
|
|
+ std::scoped_lock lock(_mutex);
|
|
+ _commandEncoders.push_back(encoder);
|
|
+ }
|
|
+ return encoder;
|
|
+};
|
|
+
|
|
+void Indium::PrivateCommandBuffer::deferredBegin() {
|
|
+ std::lock_guard<std::mutex> lock(_deferredMutex);
|
|
+ ++_deferredPending;
|
|
+}
|
|
+
|
|
+void Indium::PrivateCommandBuffer::deferredEnd() {
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_deferredMutex);
|
|
+ --_deferredPending;
|
|
+ }
|
|
+ _deferredCondvar.notify_all();
|
|
+}
|
|
+
|
|
+void Indium::PrivateCommandBuffer::waitDeferred() {
|
|
+ std::unique_lock<std::mutex> lock(_deferredMutex);
|
|
+ Waits::blockOn("waitDeferred", _deferredCondvar, lock, [this] { return _deferredPending == 0; });
|
|
+}
|
|
+
|
|
+std::shared_ptr<Indium::RenderCommandEncoder> Indium::PrivateCommandBuffer::renderCommandEncoder(const RenderPassDescriptor& descriptor) {
|
|
+ if (AsyncEncode::enabled()) {
|
|
+ auto encoder = std::make_shared<AsyncEncode::DeferredRenderCommandEncoder>(shared_from_this(), descriptor);
|
|
+ std::scoped_lock lock(_mutex);
|
|
+ _commandEncoders.push_back(encoder);
|
|
+ return encoder;
|
|
+ }
|
|
+ waitDeferred(); // an earlier encoder of this buffer may still be waiting for the worker
|
|
+ return renderCommandEncoderNow(descriptor);
|
|
+};
|
|
+
|
|
std::shared_ptr<Indium::BlitCommandEncoder> Indium::PrivateCommandBuffer::blitCommandEncoder() {
|
|
+ // the opt-in flag is tested first: AsyncEncode::enabled() bumps a shared call counter (and re-reads its flag files
|
|
+ // every 256 calls), which the default path must not pay or perturb
|
|
+ if (AsyncEncode::deferBlitEnabled() && AsyncEncode::enabled()) return blitCommandEncoder(BlitPassDescriptor {});
|
|
+ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed
|
|
auto encoder = std::make_shared<Indium::PrivateBlitCommandEncoder>(shared_from_this());
|
|
{
|
|
std::scoped_lock lock(_mutex);
|
|
@@ -66,6 +318,16 @@ std::shared_ptr<Indium::BlitCommandEncoder> Indium::PrivateCommandBuffer::blitCo
|
|
};
|
|
|
|
std::shared_ptr<Indium::BlitCommandEncoder> Indium::PrivateCommandBuffer::blitCommandEncoder(const BlitPassDescriptor& descriptor) {
|
|
+ if (AsyncEncode::deferBlitEnabled() && AsyncEncode::enabled()) {
|
|
+ // records on the caller, replays on the ordered worker: no wait, no vkCmd* on the game's thread
|
|
+ auto encoder = std::make_shared<AsyncEncode::DeferredBlitCommandEncoder>(shared_from_this(), descriptor);
|
|
+ {
|
|
+ std::scoped_lock lock(_mutex);
|
|
+ _commandEncoders.push_back(encoder);
|
|
+ }
|
|
+ return encoder;
|
|
+ }
|
|
+ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed
|
|
auto encoder = std::make_shared<Indium::PrivateBlitCommandEncoder>(shared_from_this(), descriptor);
|
|
{
|
|
std::scoped_lock lock(_mutex);
|
|
@@ -79,6 +341,7 @@ std::shared_ptr<Indium::ComputeCommandEncoder> Indium::PrivateCommandBuffer::com
|
|
};
|
|
|
|
std::shared_ptr<Indium::ComputeCommandEncoder> Indium::PrivateCommandBuffer::computeCommandEncoder(const ComputePassDescriptor& descriptor) {
|
|
+ waitDeferred(); // never record into the Vulkan command buffer while deferred render encoders are being replayed
|
|
auto encoder = std::make_shared<Indium::PrivateComputeCommandEncoder>(shared_from_this(), descriptor);
|
|
{
|
|
std::scoped_lock lock(_mutex);
|
|
@@ -91,10 +354,237 @@ std::shared_ptr<Indium::ComputeCommandEncoder> Indium::PrivateCommandBuffer::com
|
|
return computeCommandEncoder(ComputePassDescriptor { {}, dispatchType });
|
|
};
|
|
|
|
+namespace {
|
|
+// Pending merged submission. Everything is guarded by the device's graphicsQueueMutex (held by every submitter).
|
|
+struct PendingSubmit {
|
|
+ VkCommandBuffer commandBuffer;
|
|
+ std::vector<VkSemaphoreSubmitInfo> waits;
|
|
+ std::vector<VkSemaphoreSubmitInfo> signals;
|
|
+ FrameIntervalSample* frameSample = nullptr;
|
|
+};
|
|
+
|
|
+struct SubmitBatchState {
|
|
+ std::vector<PendingSubmit> items;
|
|
+ std::map<VkSemaphore, uint64_t> signaled; // timeline value signalled inside the pending batch
|
|
+ std::shared_ptr<Indium::PrivateDevice> device;
|
|
+ VkCommandPool barrierPool = VK_NULL_HANDLE;
|
|
+ VkCommandBuffer barrierCommandBuffer = VK_NULL_HANDLE;
|
|
+ std::chrono::steady_clock::time_point since;
|
|
+ bool timerStarted = false;
|
|
+};
|
|
+
|
|
+SubmitBatchState& submitBatchState() {
|
|
+ static SubmitBatchState* state = new SubmitBatchState;
|
|
+ return *state;
|
|
+}
|
|
+
|
|
+std::condition_variable& submitBatchWake() {
|
|
+ static std::condition_variable* cv = new std::condition_variable;
|
|
+ return *cv;
|
|
+}
|
|
+
|
|
+VkCommandBuffer submitBatchBarrier(const std::shared_ptr<Indium::PrivateDevice>& device) {
|
|
+ auto& state = submitBatchState();
|
|
+ if (state.barrierCommandBuffer) return state.barrierCommandBuffer;
|
|
+ // Queue submission must not acquire the upload pool lock: uploaders take that lock before the queue lock.
|
|
+ // Keep the immutable batch barrier in its own process-lifetime pool, guarded by the queue mutex.
|
|
+ VkCommandPoolCreateInfo poolInfo {};
|
|
+ poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
|
+ poolInfo.queueFamilyIndex = *device->graphicsQueueFamilyIndex();
|
|
+ if (Indium::DynamicVK::vkCreateCommandPool(device->device(), &poolInfo, nullptr, &state.barrierPool) != VK_SUCCESS) abort();
|
|
+ VkCommandBufferAllocateInfo allocInfo {};
|
|
+ allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
+ allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
+ allocInfo.commandPool = state.barrierPool;
|
|
+ allocInfo.commandBufferCount = 1;
|
|
+ VkCommandBuffer cb = VK_NULL_HANDLE;
|
|
+ if (Indium::DynamicVK::vkAllocateCommandBuffers(device->device(), &allocInfo, &cb) != VK_SUCCESS) abort();
|
|
+ VkCommandBufferBeginInfo begin {};
|
|
+ begin.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
+ begin.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
|
|
+ Indium::DynamicVK::vkBeginCommandBuffer(cb, &begin);
|
|
+ VkMemoryBarrier barrier {};
|
|
+ barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
|
+ barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
|
+ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
+ Indium::DynamicVK::vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
|
|
+ Indium::DynamicVK::vkEndCommandBuffer(cb);
|
|
+ state.barrierCommandBuffer = cb;
|
|
+ return cb;
|
|
+}
|
|
+
|
|
+// caller holds the graphics queue mutex
|
|
+void flushSubmitBatchLocked() {
|
|
+ auto& state = submitBatchState();
|
|
+ if (state.items.empty()) return;
|
|
+ auto device = state.device;
|
|
+ auto items = std::move(state.items);
|
|
+ state.items.clear();
|
|
+ state.signaled.clear();
|
|
+
|
|
+ // one batch: external waits (timeline waits deduplicated to the highest value), all signals (highest value per timeline)
|
|
+ std::map<VkSemaphore, VkSemaphoreSubmitInfo> waitTimeline, signalTimeline;
|
|
+ std::vector<VkSemaphoreSubmitInfo> waitBinary, signalBinary;
|
|
+ std::vector<VkCommandBufferSubmitInfo> commandBuffers;
|
|
+ const VkCommandBuffer barrier = items.size() > 1 ? submitBatchBarrier(device) : VK_NULL_HANDLE;
|
|
+ for (size_t i = 0; i < items.size(); ++i) {
|
|
+ if (i > 0) {
|
|
+ VkCommandBufferSubmitInfo barrierInfo {};
|
|
+ barrierInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
|
|
+ barrierInfo.commandBuffer = barrier;
|
|
+ commandBuffers.push_back(barrierInfo);
|
|
+ }
|
|
+ VkCommandBufferSubmitInfo info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
|
|
+ info.commandBuffer = items[i].commandBuffer;
|
|
+ commandBuffers.push_back(info);
|
|
+ for (const auto& w: items[i].waits) {
|
|
+ if (w.value == 0) { waitBinary.push_back(w); continue; }
|
|
+ auto it = waitTimeline.find(w.semaphore);
|
|
+ if (it == waitTimeline.end() || it->second.value < w.value) waitTimeline[w.semaphore] = w;
|
|
+ }
|
|
+ for (const auto& sg: items[i].signals) {
|
|
+ if (sg.value == 0) { signalBinary.push_back(sg); continue; }
|
|
+ auto it = signalTimeline.find(sg.semaphore);
|
|
+ if (it == signalTimeline.end() || it->second.value < sg.value) signalTimeline[sg.semaphore] = sg;
|
|
+ }
|
|
+ }
|
|
+ std::vector<VkSemaphoreSubmitInfo> waits = waitBinary, signals = signalBinary;
|
|
+ for (const auto& [sem, w]: waitTimeline) waits.push_back(w);
|
|
+ for (const auto& [sem, sg]: signalTimeline) signals.push_back(sg);
|
|
+
|
|
+ VkSubmitInfo2 info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
|
|
+ info.commandBufferInfoCount = commandBuffers.size();
|
|
+ info.pCommandBufferInfos = commandBuffers.data();
|
|
+ info.signalSemaphoreInfoCount = signals.size();
|
|
+ info.pSignalSemaphoreInfos = signals.data();
|
|
+ info.waitSemaphoreInfoCount = waits.size();
|
|
+ info.pWaitSemaphoreInfos = waits.data();
|
|
+ Indium::DynamicVK::beginDiagnosticCapture(Indium::globalInstance);
|
|
+ if (Indium::DynamicVK::vkQueueSubmit2(device->graphicsQueue(), 1, &info, VK_NULL_HANDLE) != VK_SUCCESS) abort();
|
|
+ statSubmits.fetch_add(1, std::memory_order_relaxed);
|
|
+ Indium::DynamicVK::endDiagnosticCapture(false);
|
|
+ Indium::count("batchedSubmits", 1);
|
|
+ Indium::count("batchedCommandBuffers", items.size());
|
|
+ for (auto& item: items) {
|
|
+ if (item.frameSample) item.frameSample->submitted.store(frameIntervalNow(), std::memory_order_release);
|
|
+ }
|
|
+}
|
|
+
|
|
+void startSubmitBatchTimer(const std::shared_ptr<Indium::PrivateDevice>& device) {
|
|
+ auto& state = submitBatchState();
|
|
+ if (state.timerStarted) return;
|
|
+ state.timerStarted = true;
|
|
+ std::thread([device]() {
|
|
+ auto& state = submitBatchState();
|
|
+ std::mutex wakeMutex;
|
|
+ for (;;) {
|
|
+ {
|
|
+ std::unique_lock<std::mutex> lock(wakeMutex);
|
|
+ /* darling-lol perf: 500µs batch window balances submission
|
|
+ * merging with latency; the old 2ms added visible input lag. */
|
|
+ submitBatchWake().wait_for(lock, std::chrono::microseconds(500));
|
|
+ }
|
|
+ std::unique_lock<std::mutex> queueLock(device->graphicsQueueMutex());
|
|
+ if (!state.items.empty() && std::chrono::steady_clock::now() - state.since >= std::chrono::microseconds(500)) flushSubmitBatchLocked();
|
|
+ }
|
|
+ }).detach();
|
|
+}
|
|
+}
|
|
+
|
|
+// ON by default. League commits 29-38 command buffers per frame (one per render pass); submitted one by one, each is an
|
|
+// amdgpu_cs ioctl. Merged they become 3-4 submissions per frame: main-thread kernel time 12.1% -> 7.1% of wall time in
|
|
+// a Baron fight (three interleaved 8 s windows each), fight 67.8 -> 70.9 fps, auto-attack 89 -> 96+, idle 98 -> 102
|
|
+// (2026-10-10, tools/batch-ab.sh). Live switch: guest file /tmp/metal-no-submit-batch turns it off, re-read every 512 calls.
|
|
+bool Indium::submitBatchEnabled() {
|
|
+ static std::atomic<bool> enabled {true};
|
|
+ static std::atomic<unsigned> calls {0};
|
|
+ if ((calls.fetch_add(1, std::memory_order_relaxed) & 511) == 0)
|
|
+ enabled.store(access("/tmp/metal-no-submit-batch", F_OK) != 0, std::memory_order_relaxed);
|
|
+ return enabled.load(std::memory_order_relaxed);
|
|
+}
|
|
+
|
|
+void Indium::flushPendingSubmits() {
|
|
+ auto& state = submitBatchState();
|
|
+ std::shared_ptr<Indium::PrivateDevice> device = state.device; // set under the queue mutex by the first batched commit
|
|
+ if (!device) return;
|
|
+ // runs on the game thread (waitUntilCompleted) and on the upload worker; the counter attributes blocks to their caller.
|
|
+ if (Waits::enabled() && !SubmitThread::onWorker()) {
|
|
+ Waits::blockOnLock("queueMutex", device->graphicsQueueMutex());
|
|
+ std::unique_lock<std::mutex> queueLock(device->graphicsQueueMutex(), std::adopt_lock);
|
|
+ flushSubmitBatchLocked();
|
|
+ return;
|
|
+ }
|
|
+ std::unique_lock<std::mutex> queueLock(device->graphicsQueueMutex());
|
|
+ flushSubmitBatchLocked();
|
|
+}
|
|
+
|
|
+namespace {
|
|
+std::atomic<bool> bindingReuse{true};
|
|
+void refreshBindingReuse() {
|
|
+ static std::atomic<unsigned> calls{0};
|
|
+ // A bounded diagnostic switch for same-process combat A/B. Do not perform
|
|
+ // filesystem lookups in the per-draw/per-constant hot paths.
|
|
+ if ((calls.fetch_add(1,std::memory_order_relaxed) & 63)==0)
|
|
+ bindingReuse.store(access("/tmp/metal-no-binding-reuse",F_OK)!=0,std::memory_order_relaxed);
|
|
+}
|
|
+// OFF by default (round-3 behaviour: a presenting commit waits for the whole worker queue with drain()). Guest flag file
|
|
+// /tmp/metal-present-dispatch-wait (checked once per process) makes the frame-end sync narrower: the caller waits only
|
|
+// until this commit's own task has run (its present dispatch, and by FIFO order every replay queued before it), not for
|
|
+// tasks queued after it. Round 4 had this on by default; that round regressed in matches, so it is opt-in now.
|
|
+bool presentDispatchWaitEnabled() {
|
|
+ static const bool value = access("/tmp/metal-present-dispatch-wait", F_OK) == 0;
|
|
+ return value;
|
|
+}
|
|
+}
|
|
+bool Indium::bindingReuseEnabled() {return bindingReuse.load(std::memory_order_relaxed);}
|
|
+
|
|
void Indium::PrivateCommandBuffer::commit() {
|
|
+ refreshBindingReuse();
|
|
+ // With deferred render encoding the encoders are applied on the ordered worker, so the whole commit (it reads what the
|
|
+ // encoders recorded, ends the Vulkan command buffer and reserves timeline values in submission order) must run there too.
|
|
+ // It also has to when earlier commits are still queued: submission order is commit order.
|
|
+ if (!SubmitThread::onWorker() && (AsyncEncode::enabled() || SubmitThread::pending())) {
|
|
+ auto self = shared_from_this();
|
|
+ // read on the calling thread: the worker's commit consumes the list
|
|
+ const bool presents = !_drawablesToPresent.empty();
|
|
+ // Frame-end sync: the present is queued to the main queue by the worker's commit. The game pumps its run loop
|
|
+ // right after this call, so let the worker catch up first (it ran in parallel during the frame, the wait is its
|
|
+ // remaining lag); otherwise the present misses this pump and is shown one frame late, sometimes two per pump.
|
|
+ // Guest flag file /tmp/metal-async-no-frame-sync disables it for A/B runs.
|
|
+ if (presents && presentDispatchWaitEnabled() && access("/tmp/metal-async-no-frame-sync", F_OK) != 0) {
|
|
+ // Opt-in narrow wait (/tmp/metal-present-dispatch-wait): this commit's task signals the token when it is
|
|
+ // done, i.e. when the present dispatch (and, because the worker is FIFO, every replay queued before it) has
|
|
+ // happened. Waiting for the token instead of the queue's idle state does not block on later tasks.
|
|
+ SubmitThread::Token token;
|
|
+ SubmitThread::post([self, &token]() {
|
|
+ struct Signal {
|
|
+ SubmitThread::Token* token;
|
|
+ ~Signal() { token->signal(); }
|
|
+ } signal { &token };
|
|
+ AsyncEncode::PoolScope pool;
|
|
+ self->commit();
|
|
+ });
|
|
+ token.wait("presentDispatchWait");
|
|
+ return;
|
|
+ }
|
|
+ // Default (round 3): post first, then check the flag file and wait for the whole worker queue.
|
|
+ SubmitThread::post([self]() {
|
|
+ AsyncEncode::PoolScope pool;
|
|
+ self->commit();
|
|
+ });
|
|
+ if (presents && access("/tmp/metal-async-no-frame-sync", F_OK) != 0) {
|
|
+ SubmitThread::drain("drain@commit");
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+ auto* frameSample = beginFrameIntervalSample(!_drawablesToPresent.empty());
|
|
+ // with /tmp/metal-worker-lease the commit is the last recorder of this command buffer, so it leases it when nothing
|
|
+ // did before (empty buffer); by default the constructor has leased it
|
|
+ if (workerLeaseEnabled()) ensureRecordingStarted();
|
|
std::unique_lock lock(_mutex);
|
|
|
|
_committed = true;
|
|
+ _committedAtomic.store(true, std::memory_order_release);
|
|
|
|
for (const auto& encoder: _commandEncoders) {
|
|
if (auto renderEncoder = std::dynamic_pointer_cast<PrivateRenderCommandEncoder>(encoder)) {
|
|
@@ -110,16 +600,28 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
}
|
|
}
|
|
|
|
+ if (auto blitEncoder = std::dynamic_pointer_cast<PrivateBlitCommandEncoder>(encoder)) {
|
|
+ for (const auto& texture: blitEncoder->readWriteTextures()) {
|
|
+ if (auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture)) {
|
|
+ privateTexture->precommit(shared_from_this());
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
// TODO: same for other encoders
|
|
}
|
|
|
|
+ GpuTiming::endSpan(_commandBuffer);
|
|
if (DynamicVK::vkEndCommandBuffer(_commandBuffer) != VK_SUCCESS) {
|
|
// TODO
|
|
abort();
|
|
}
|
|
|
|
- // to keep ourselves alive until we're done
|
|
+ // Everything below talks to the Vulkan queue and runs on the ordered submit worker (see submit-thread.private.hpp);
|
|
+ // this thread only had to finish recording. The task keeps the command buffer alive until it is done.
|
|
+ lock.unlock();
|
|
auto self = shared_from_this();
|
|
+ auto submitTask = [this, self, frameSample]() {
|
|
|
|
// for the event loop
|
|
auto timelineSemaphore = _privateDevice->getWrappedTimelineSemaphore();
|
|
@@ -138,14 +640,49 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
readWriteTextures.insert(readWriteTextures.end(), readWrite.begin(), readWrite.end());
|
|
}
|
|
|
|
- // TODO: implement this for other encoders (we need to synchronize those texture accesses as well)
|
|
+ if (auto blitEncoder = std::dynamic_pointer_cast<PrivateBlitCommandEncoder>(encoder)) {
|
|
+ // only the writes to drawables are tracked for now, so that blitting into a drawable is synchronized with its presentation
|
|
+ auto& readWrite = blitEncoder->readWriteTextures();
|
|
+ readWriteTextures.insert(readWriteTextures.end(), readWrite.begin(), readWrite.end());
|
|
+ }
|
|
+
|
|
+ // TODO: implement this for the other texture accesses of the blit and compute encoders (we need to synchronize those as well)
|
|
}
|
|
|
|
+ // Views share synchronization with their root texture. Deduplicate that owner,
|
|
+ // including read/write overlap, before locking or reserving timeline values.
|
|
+ auto rootTexture = [](std::shared_ptr<Texture> texture) {
|
|
+ while (auto parent = texture->parentTexture()) texture = parent;
|
|
+ return texture;
|
|
+ };
|
|
+ auto canonicalize = [&](std::vector<std::shared_ptr<Texture>>& textures) {
|
|
+ std::vector<std::shared_ptr<Texture>> unique;
|
|
+ for (const auto& texture : textures) {
|
|
+ auto root = rootTexture(texture);
|
|
+ if (std::find(unique.begin(), unique.end(), root) == unique.end()) unique.push_back(root);
|
|
+ }
|
|
+ textures = std::move(unique);
|
|
+ };
|
|
+ canonicalize(readWriteTextures);
|
|
+ canonicalize(readOnlyTextures);
|
|
+ readOnlyTextures.erase(std::remove_if(readOnlyTextures.begin(), readOnlyTextures.end(),
|
|
+ [&](const auto& texture) { return std::find(readWriteTextures.begin(), readWriteTextures.end(), texture) != readWriteTextures.end(); }), readOnlyTextures.end());
|
|
+
|
|
+ // Reserve texture timeline values in submission order. Locking only the
|
|
+ // Vulkan call would let a later waiter be submitted ahead of its signal.
|
|
+ std::unique_lock<std::mutex> queueLock(_privateDevice->graphicsQueueMutex());
|
|
+
|
|
for (const auto& texture: readWriteTextures) {
|
|
auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
|
- auto sema = _privateDevice->getWrappedBinarySemaphore(privateTexture->needsExportablePresentationSemaphore());
|
|
+ // Ordinary offscreen textures synchronize through their timeline semaphore.
|
|
+ // Their presentation binary semaphore has no consumer. Keep it for
|
|
+ // drawable subclasses (including the native non-exportable swapchain).
|
|
+ std::shared_ptr<BinarySemaphore> sema;
|
|
+ if (!std::dynamic_pointer_cast<ConcreteTexture>(privateTexture)) {
|
|
+ sema = _privateDevice->getWrappedBinarySemaphore(privateTexture->needsExportablePresentationSemaphore());
|
|
+ privateTexture->beginUpdatingPresentationSemaphore(sema);
|
|
+ }
|
|
presentationSemaphores.push_back(sema);
|
|
- privateTexture->beginUpdatingPresentationSemaphore(sema);
|
|
}
|
|
|
|
std::vector<VkSemaphoreSubmitInfo> signalInfos;
|
|
@@ -202,11 +739,13 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
|
const auto& presentSema = presentationSemaphores[i];
|
|
|
|
- VkSemaphoreSubmitInfo signalInfo {};
|
|
- signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
|
|
- signalInfo.semaphore = presentSema->semaphore;
|
|
- signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
|
|
- signalInfos.push_back(signalInfo);
|
|
+ if (presentSema) {
|
|
+ VkSemaphoreSubmitInfo signalInfo {};
|
|
+ signalInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
|
|
+ signalInfo.semaphore = presentSema->semaphore;
|
|
+ signalInfo.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
|
|
+ signalInfos.push_back(signalInfo);
|
|
+ }
|
|
|
|
handleTextureSemaphores(privateTexture);
|
|
}
|
|
@@ -215,7 +754,15 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
// but it also leaves the resources for this command buffer tied up, essentially becoming a memory leak.
|
|
// i've observed this in the cube example, and it happens more than once (because the example display semaphore is exhausted and never signaled).
|
|
// UPDATE: upon further testing, it seems that this only occurs when the view is off-screen/hidden. weird.
|
|
- _privateDevice->waitForSemaphore(timelineSemaphore->semaphore, timelineSemaphore->count, [self, timelineSemaphore, extraWaitSemaphores, presentationSemaphores]() {
|
|
+ const double submitTime = indiumTimingEnabled() ? indiumNow() : 0;
|
|
+ _privateDevice->waitForSemaphore(timelineSemaphore->semaphore, timelineSemaphore->count, [self, timelineSemaphore, extraWaitSemaphores, presentationSemaphores, submitTime, frameSample]() {
|
|
+ if (frameSample) frameSample->completed.store(frameIntervalNow(), std::memory_order_release);
|
|
+ double callbackStart = 0;
|
|
+ if (submitTime != 0) {
|
|
+ callbackStart = indiumNow();
|
|
+ indiumTiming("submitToCallback", callbackStart - submitTime);
|
|
+ }
|
|
+ GpuTiming::collect(self->_privateDevice->device(), self->_commandBuffer, self->_privateDevice->properties().limits.timestampPeriod);
|
|
{
|
|
std::unique_lock lock(self->_mutex);
|
|
self->_completed = true;
|
|
@@ -234,8 +781,47 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
// the handlers may have captured a reference to us (from their surrounding scope), so clear out handlers so those references go away
|
|
self->_scheduledHandlers.clear();
|
|
self->_completedHandlers.clear();
|
|
+
|
|
+ if (submitTime != 0) {
|
|
+ indiumTiming("handlersDuration", indiumNow() - callbackStart);
|
|
+ }
|
|
});
|
|
|
|
+ statCommits.fetch_add(1, std::memory_order_relaxed);
|
|
+ if (!_drawablesToPresent.empty()) submitStatsPresented();
|
|
+ if (Indium::submitBatchEnabled() && _drawablesToPresent.empty()) {
|
|
+ bool batchable = true;
|
|
+ for (const auto& ps: presentationSemaphores) if (ps) { batchable = false; break; }
|
|
+ if (batchable) {
|
|
+ auto& batch = submitBatchState();
|
|
+ batch.device = _privateDevice;
|
|
+ PendingSubmit item;
|
|
+ item.commandBuffer = _commandBuffer;
|
|
+ item.frameSample = frameSample;
|
|
+ for (const auto& w: waitInfos) {
|
|
+ // a timeline value signalled earlier in this batch is guaranteed by the order (and the barrier) inside it
|
|
+ if (w.value != 0) {
|
|
+ auto it = batch.signaled.find(w.semaphore);
|
|
+ if (it != batch.signaled.end() && it->second >= w.value) continue;
|
|
+ }
|
|
+ item.waits.push_back(w);
|
|
+ }
|
|
+ item.signals = signalInfos;
|
|
+ if (batch.items.empty()) batch.since = std::chrono::steady_clock::now();
|
|
+ for (const auto& sg: signalInfos) {
|
|
+ if (sg.value == 0) continue;
|
|
+ auto& v = batch.signaled[sg.semaphore];
|
|
+ if (v < sg.value) v = sg.value;
|
|
+ }
|
|
+ batch.items.push_back(std::move(item));
|
|
+ startSubmitBatchTimer(_privateDevice);
|
|
+ if (batch.items.size() >= 16) flushSubmitBatchLocked();
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ // not batched: everything committed earlier goes first so the submission order is preserved
|
|
+ flushSubmitBatchLocked();
|
|
+
|
|
commandBufferInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
|
|
commandBufferInfo.commandBuffer = _commandBuffer;
|
|
|
|
@@ -252,23 +838,41 @@ void Indium::PrivateCommandBuffer::commit() {
|
|
// the Device constructor tries to choose command queues that support as many operations as possible, but it's possible
|
|
// that a particular device only supports certain operations on certain queues (e.g. maybe it only supports transfer operations
|
|
// on an exclusive queue that doesn't support graphics or compute).
|
|
- if (DynamicVK::vkQueueSubmit2(_privateDevice->graphicsQueue(), 1, &info, VK_NULL_HANDLE) != VK_SUCCESS) {
|
|
+ const double submitCallStart = submitTime != 0 ? indiumNow() : 0;
|
|
+ if (submitTime != 0) {
|
|
+ indiumTiming("waitSemaphoresPerSubmit", waitInfos.size() / 1000.0);
|
|
+ }
|
|
+ DynamicVK::beginDiagnosticCapture(Indium::globalInstance);
|
|
+ VkResult submitResult = DynamicVK::vkQueueSubmit2(_privateDevice->graphicsQueue(), 1, &info, VK_NULL_HANDLE);
|
|
+ statSubmits.fetch_add(1, std::memory_order_relaxed);
|
|
+ if (submitResult != VK_SUCCESS) {
|
|
// TODO
|
|
abort();
|
|
}
|
|
|
|
- lock.unlock();
|
|
+ if (frameSample) frameSample->submitted.store(frameIntervalNow(), std::memory_order_release);
|
|
+ if (submitTime != 0) {
|
|
+ indiumTiming("vkQueueSubmit2", indiumNow() - submitCallStart);
|
|
+ }
|
|
+
|
|
|
|
// now that the binary semaphore signals are pending, we can allow them to be used
|
|
- for (const auto& texture: readWriteTextures) {
|
|
- auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(texture);
|
|
- privateTexture->endUpdatingPresentationSemaphore();
|
|
+ for (size_t i = 0; i < readWriteTextures.size(); ++i) {
|
|
+ if (presentationSemaphores[i]) {
|
|
+ auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(readWriteTextures[i]);
|
|
+ privateTexture->endUpdatingPresentationSemaphore();
|
|
+ }
|
|
}
|
|
+ queueLock.unlock();
|
|
|
|
// we can now queue drawables for presentation and they'll be synchronized properly
|
|
for (const auto& drawable: _drawablesToPresent) {
|
|
drawable->present();
|
|
}
|
|
+ if (frameSample) frameSample->presented.store(frameIntervalNow(), std::memory_order_release);
|
|
+ DynamicVK::endDiagnosticCapture(!_drawablesToPresent.empty());
|
|
+ };
|
|
+ SubmitThread::run(std::move(submitTask));
|
|
};
|
|
|
|
void Indium::PrivateCommandBuffer::presentDrawable(std::shared_ptr<Drawable> drawable) {
|
|
@@ -307,9 +911,12 @@ void Indium::PrivateCommandBuffer::addCompletedHandler(std::function<void(std::s
|
|
};
|
|
|
|
void Indium::PrivateCommandBuffer::waitUntilCompleted() {
|
|
+ SubmitThread::drain("drain@waitUntilCompleted"); // a commit posted to the worker has not necessarily been submitted yet
|
|
+ Indium::flushPendingSubmits();
|
|
+ // diagnostic counter (guest /tmp/metal-debug-timing flag): the calling thread blocks until the GPU has finished this buffer
|
|
+ const auto waitStart = std::chrono::steady_clock::now();
|
|
std::unique_lock lock(_mutex);
|
|
|
|
- while (!_completed) {
|
|
- _completedCondvar.wait(lock);
|
|
- }
|
|
+ Waits::blockOn("waitUntilCompleted", _completedCondvar, lock, [this] { return _completed; });
|
|
+ Indium::count("waitUntilCompletedMicroseconds", std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - waitStart).count());
|
|
};
|
|
diff --git a/src/indium/command-queue.cpp b/src/indium/command-queue.cpp
|
|
index 5704602..d2cd020 100644
|
|
--- a/src/indium/command-queue.cpp
|
|
+++ b/src/indium/command-queue.cpp
|
|
@@ -22,6 +22,7 @@ Indium::PrivateCommandQueue::PrivateCommandQueue(std::shared_ptr<PrivateDevice>
|
|
// TODO: handle this in a more C++-friendly way
|
|
abort();
|
|
}
|
|
+ _availablePools.push_back({_commandPool, VK_NULL_HANDLE});
|
|
|
|
_supportsGraphics = !!_privateDevice->graphicsQueueFamilyIndex();
|
|
_supportsCompute = _supportsGraphics ? (_privateDevice->computeQueueFamilyIndex() ? *_privateDevice->computeQueueFamilyIndex() == *_privateDevice->graphicsQueueFamilyIndex() : false) : !!_privateDevice->computeQueueFamilyIndex();
|
|
@@ -29,11 +30,36 @@ Indium::PrivateCommandQueue::PrivateCommandQueue(std::shared_ptr<PrivateDevice>
|
|
};
|
|
|
|
Indium::PrivateCommandQueue::~PrivateCommandQueue() {
|
|
- if (_commandPool) {
|
|
- DynamicVK::vkDestroyCommandPool(_privateDevice->device(), _commandPool, nullptr);
|
|
+ for (auto pool: _availablePools) {
|
|
+ DynamicVK::vkDestroyCommandPool(_privateDevice->device(), pool.pool, nullptr);
|
|
}
|
|
};
|
|
|
|
+VkCommandPool Indium::PrivateCommandQueue::acquireCommandPool(VkCommandBuffer& buffer) {
|
|
+ std::lock_guard<std::mutex> lock(_poolMutex);
|
|
+ if (!_availablePools.empty()) {
|
|
+ auto pool = _availablePools.back();
|
|
+ _availablePools.pop_back();
|
|
+ // The previous owner retains this exclusive lease through GPU completion.
|
|
+ // Reset without RELEASE_RESOURCES so RADV can reuse its recording storage.
|
|
+ if (DynamicVK::vkResetCommandPool(_privateDevice->device(), pool.pool, 0) != VK_SUCCESS) abort();
|
|
+ buffer = pool.buffer;
|
|
+ return pool.pool;
|
|
+ }
|
|
+ VkCommandPoolCreateInfo info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
|
|
+ info.queueFamilyIndex = _privateDevice->graphicsQueueFamilyIndex() ? *_privateDevice->graphicsQueueFamilyIndex() : *_privateDevice->computeQueueFamilyIndex();
|
|
+ buffer = VK_NULL_HANDLE;
|
|
+ VkCommandPool pool = VK_NULL_HANDLE;
|
|
+ if (DynamicVK::vkCreateCommandPool(_privateDevice->device(), &info, nullptr, &pool) != VK_SUCCESS) abort();
|
|
+ return pool;
|
|
+}
|
|
+
|
|
+void Indium::PrivateCommandQueue::releaseCommandPool(VkCommandPool pool, VkCommandBuffer buffer) {
|
|
+ std::lock_guard<std::mutex> lock(_poolMutex);
|
|
+ _availablePools.push_back({pool, buffer});
|
|
+}
|
|
+
|
|
std::shared_ptr<Indium::CommandBuffer> Indium::PrivateCommandQueue::commandBuffer() {
|
|
return std::make_shared<PrivateCommandBuffer>(shared_from_this());
|
|
};
|
|
diff --git a/src/indium/compute-command-encoder.cpp b/src/indium/compute-command-encoder.cpp
|
|
index 5f62ad0..5957bec 100644
|
|
--- a/src/indium/compute-command-encoder.cpp
|
|
+++ b/src/indium/compute-command-encoder.cpp
|
|
@@ -9,27 +9,19 @@ Indium::PrivateComputeCommandEncoder::PrivateComputeCommandEncoder(std::shared_p
|
|
_privateDevice(std::dynamic_pointer_cast<PrivateDevice>(commandBuffer->device())),
|
|
_descriptor(descriptor)
|
|
{
|
|
- VkDescriptorPoolCreateInfo poolCreateInfo {};
|
|
- poolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
- poolCreateInfo.poolSizeCount = poolSizes.size();
|
|
- poolCreateInfo.pPoolSizes = poolSizes.data();
|
|
- poolCreateInfo.maxSets = 64;
|
|
-
|
|
- if (DynamicVK::vkCreateDescriptorPool(_privateDevice->device(), &poolCreateInfo, nullptr, &_pool) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
+ _pool = newDescriptorPool(_privateDevice);
|
|
};
|
|
|
|
Indium::PrivateComputeCommandEncoder::~PrivateComputeCommandEncoder() {
|
|
for (auto& pipeline: _savedPipelines) {
|
|
DynamicVK::vkDestroyPipeline(_privateDevice->device(), pipeline, nullptr);
|
|
}
|
|
- DynamicVK::vkDestroyDescriptorPool(_privateDevice->device(), _pool, 0);
|
|
+ _privateDevice->releaseDescriptorPool(_pool);
|
|
+ for (auto pool : _retiredPools) _privateDevice->releaseDescriptorPool(pool);
|
|
};
|
|
|
|
void Indium::PrivateComputeCommandEncoder::endEncoding() {
|
|
- // nothing for now
|
|
+ finishTrackedEncoder(_privateCommandBuffer.lock()->commandBuffer());
|
|
};
|
|
|
|
void Indium::PrivateComputeCommandEncoder::setComputePipelineState(std::shared_ptr<ComputePipelineState> state) {
|
|
@@ -138,7 +130,7 @@ Indium::DispatchType Indium::PrivateComputeCommandEncoder::dispatchType() {
|
|
void Indium::PrivateComputeCommandEncoder::updateBindings() {
|
|
auto buf = _privateCommandBuffer.lock();
|
|
|
|
- std::array<VkDescriptorSet, 1> descriptorSets = createDescriptorSets(_pso->descriptorSetLayouts().layouts, _pool, _privateDevice, { _functionResources }, { _pso->functionInfo() }, _keepAliveBuffers);
|
|
+ std::array<VkDescriptorSet, 1> descriptorSets = createDescriptorSets(_pso->descriptorSetLayouts().layouts, _pool, _retiredPools, _privateDevice, { _functionResources }, { _pso->functionInfo() }, _keepAliveBuffers, _addressPool);
|
|
|
|
DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_COMPUTE, _pso->layout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr);
|
|
};
|
|
diff --git a/src/indium/device.cpp b/src/indium/device.cpp
|
|
index 322e186..78b77ae 100644
|
|
--- a/src/indium/device.cpp
|
|
+++ b/src/indium/device.cpp
|
|
@@ -1,4 +1,6 @@
|
|
#include <indium/device.private.hpp>
|
|
+#include <indium/submit-thread.private.hpp>
|
|
+#include <indium/command-encoder.private.hpp>
|
|
#include <indium/instance.private.hpp>
|
|
#include <indium/command-queue.private.hpp>
|
|
#include <indium/render-pipeline.private.hpp>
|
|
@@ -12,39 +14,207 @@
|
|
|
|
#include <iridium/iridium.hpp>
|
|
|
|
+#include <algorithm>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <thread>
|
|
#include <unordered_set>
|
|
+#include <utility>
|
|
|
|
+#include <cctype>
|
|
+#include <cstdio>
|
|
+#include <cstdlib>
|
|
#include <cstring>
|
|
+#include <string>
|
|
+#include <unistd.h>
|
|
#include <vector>
|
|
|
|
std::vector<std::shared_ptr<Indium::PrivateDevice>> Indium::globalDeviceList;
|
|
|
|
+namespace {
|
|
+ // One line per event, appended to the guest's /tmp/metal-device.log (host:
|
|
+ // ~/.darling/private/tmp/metal-device.log). Low volume: a few lines while the
|
|
+ // device list is built, plus one when the presenting OpenGL context first
|
|
+ // imports a Vulkan drawable. It exists so a user can tell us which GPU
|
|
+ // actually rendered a match, which the game's own logs and FPS numbers do not
|
|
+ // show.
|
|
+ void appendDeviceLog(const std::string& line) {
|
|
+ if (FILE* log = fopen("/tmp/metal-device.log", "a")) {
|
|
+ fprintf(log, "[MetalDevice] pid=%d %s\n", getpid(), line.c_str());
|
|
+ fclose(log);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ std::string hexValue(unsigned long long value, int width) {
|
|
+ char buffer[32];
|
|
+ snprintf(buffer, sizeof(buffer), "%0*llx", width, value);
|
|
+ return buffer;
|
|
+ }
|
|
+
|
|
+ std::string uuidToString(const uint8_t uuid[16]) {
|
|
+ static const char* digits = "0123456789abcdef";
|
|
+ std::string out;
|
|
+ out.reserve(32);
|
|
+ for (int i = 0; i < 16; ++i) {
|
|
+ out += digits[(uuid[i] >> 4) & 0xf];
|
|
+ out += digits[uuid[i] & 0xf];
|
|
+ }
|
|
+ return out;
|
|
+ }
|
|
+
|
|
+ const char* deviceTypeName(VkPhysicalDeviceType type) {
|
|
+ switch (type) {
|
|
+ case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "integrated";
|
|
+ case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "discrete";
|
|
+ case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "virtual";
|
|
+ case VK_PHYSICAL_DEVICE_TYPE_CPU: return "cpu";
|
|
+ default: return "other";
|
|
+ }
|
|
+ }
|
|
+
|
|
+ std::string memoryFlagsString(VkMemoryPropertyFlags flags) {
|
|
+ static const std::pair<VkMemoryPropertyFlags, const char*> names[] = {
|
|
+ { VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, "DEVICE_LOCAL" },
|
|
+ { VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, "HOST_VISIBLE" },
|
|
+ { VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, "HOST_COHERENT" },
|
|
+ { VK_MEMORY_PROPERTY_HOST_CACHED_BIT, "HOST_CACHED" },
|
|
+ { VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT, "LAZILY_ALLOCATED" },
|
|
+ { VK_MEMORY_PROPERTY_PROTECTED_BIT, "PROTECTED" },
|
|
+ { VK_MEMORY_PROPERTY_DEVICE_COHERENT_BIT_AMD, "DEVICE_COHERENT_AMD" },
|
|
+ { VK_MEMORY_PROPERTY_DEVICE_UNCACHED_BIT_AMD, "DEVICE_UNCACHED_AMD" },
|
|
+ };
|
|
+ std::string out = "0x" + hexValue(flags, 1) + "(";
|
|
+ bool first = true;
|
|
+ VkMemoryPropertyFlags rest = flags;
|
|
+ for (const auto& name: names) {
|
|
+ if ((flags & name.first) == 0) continue;
|
|
+ out += (first ? "" : "|") + std::string(name.second);
|
|
+ first = false;
|
|
+ rest &= ~name.first;
|
|
+ }
|
|
+ if (rest) out += (first ? "0x" : "|0x") + hexValue(rest, 1);
|
|
+ else if (first) out += "none";
|
|
+ return out + ")";
|
|
+ }
|
|
+
|
|
+ // The memory types and heaps of the device Metal renders on, logged once when it
|
|
+ // is selected, so the per-allocation lines below can be read without vulkaninfo.
|
|
+ void appendMemoryTable(const VkPhysicalDeviceMemoryProperties& memory) {
|
|
+ for (uint32_t i = 0; i < memory.memoryHeapCount; ++i) {
|
|
+ const auto& heap = memory.memoryHeaps[i];
|
|
+ appendDeviceLog("memory heap=" + std::to_string(i) + " size=" + std::to_string(heap.size >> 20) + "MiB"
|
|
+ + " deviceLocal=" + ((heap.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? "yes" : "no"));
|
|
+ }
|
|
+ for (uint32_t i = 0; i < memory.memoryTypeCount; ++i) {
|
|
+ const auto& type = memory.memoryTypes[i];
|
|
+ appendDeviceLog("memory type=" + std::to_string(i) + " flags=" + memoryFlagsString(type.propertyFlags)
|
|
+ + " heap=" + std::to_string(type.heapIndex));
|
|
+ }
|
|
+ }
|
|
+
|
|
+ struct GpuSelector {
|
|
+ std::string value;
|
|
+ std::string source; // empty when the loader's default order should decide
|
|
+ };
|
|
+
|
|
+ // Explicit selection, in the style of the other opt-in guest files. The guest
|
|
+ // file /tmp/metal-gpu wins over the METAL_INDIUM_GPU environment variable: the
|
|
+ // file is scoped to the guest and does not depend on the environment reaching
|
|
+ // the game through the Riot/League client. The value is either the index of a
|
|
+ // supported device or a case-insensitive substring of its Vulkan name, so
|
|
+ // "nvidia", "amd" or "llvmpipe" all work.
|
|
+ GpuSelector readGpuSelector() {
|
|
+ if (FILE* file = fopen("/tmp/metal-gpu", "r")) {
|
|
+ char buffer[256] = {0};
|
|
+ char* got = fgets(buffer, sizeof(buffer), file);
|
|
+ fclose(file);
|
|
+ if (got) {
|
|
+ std::string value(buffer);
|
|
+ while (!value.empty() && (value.back() == '\n' || value.back() == '\r' || value.back() == ' ' || value.back() == '\t')) {
|
|
+ value.pop_back();
|
|
+ }
|
|
+ if (!value.empty()) return { value, "file:/tmp/metal-gpu" };
|
|
+ }
|
|
+ }
|
|
+ if (const char* env = getenv("METAL_INDIUM_GPU")) {
|
|
+ if (env[0]) return { std::string(env), "env:METAL_INDIUM_GPU" };
|
|
+ }
|
|
+ return {};
|
|
+ }
|
|
+
|
|
+ bool isAllDigits(const std::string& value) {
|
|
+ return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
|
|
+ }
|
|
+
|
|
+ bool nameContains(const std::string& name, const std::string& needle) {
|
|
+ return std::search(name.begin(), name.end(), needle.begin(), needle.end(), [](char a, char b) {
|
|
+ return std::tolower((unsigned char) a) == std::tolower((unsigned char) b);
|
|
+ }) != name.end();
|
|
+ }
|
|
+
|
|
+ struct DeviceCandidate {
|
|
+ VkPhysicalDevice device;
|
|
+ VkPhysicalDeviceProperties properties;
|
|
+ uint8_t deviceUUID[16];
|
|
+ uint8_t driverUUID[16];
|
|
+ };
|
|
+}
|
|
+
|
|
void Indium::initGlobalDeviceList() {
|
|
std::vector<VkPhysicalDevice> physicalDevices;
|
|
uint32_t count = 0;
|
|
|
|
auto result = DynamicVK::vkEnumeratePhysicalDevices(globalInstance, &count, nullptr);
|
|
if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
|
|
- // TODO: maybe warn?
|
|
+ appendDeviceLog("vkEnumeratePhysicalDevices failed when counting devices; Metal will have no device");
|
|
return;
|
|
}
|
|
|
|
physicalDevices.resize(count);
|
|
result = DynamicVK::vkEnumeratePhysicalDevices(globalInstance, &count, physicalDevices.data());
|
|
if (result != VK_SUCCESS && result != VK_INCOMPLETE) {
|
|
- // TODO: maybe warn?
|
|
+ appendDeviceLog("vkEnumeratePhysicalDevices failed when listing devices; Metal will have no device");
|
|
return;
|
|
}
|
|
|
|
+ // The loader's order used to decide the default device (createSystemDefaultDevice
|
|
+ // returns the first entry). On a hybrid laptop that is usually the integrated
|
|
+ // GPU the display is wired to, not the discrete GPU. Record every device the
|
|
+ // loader lists, then choose explicitly.
|
|
+ GpuSelector selector = readGpuSelector();
|
|
+ const bool skipCPUDevices = access("/tmp/metal-skip-cpu-devices", F_OK) == 0;
|
|
+ const bool allDevices = access("/tmp/metal-all-devices", F_OK) == 0;
|
|
+ appendDeviceLog("selector=" + (selector.source.empty() ? std::string("none") : selector.source + " value=\"" + selector.value + "\"")
|
|
+ + " skipCPUDevices=" + (skipCPUDevices ? "1" : "0") + " allDevices=" + (allDevices ? "1" : "0")
|
|
+ + " physicalDeviceCount=" + std::to_string(physicalDevices.size()));
|
|
+
|
|
+ std::vector<DeviceCandidate> candidates;
|
|
+
|
|
for (auto&& device: physicalDevices) {
|
|
- VkPhysicalDeviceProperties props;
|
|
- DynamicVK::vkGetPhysicalDeviceProperties(device, &props);
|
|
+ VkPhysicalDeviceProperties properties;
|
|
+ DynamicVK::vkGetPhysicalDeviceProperties(device, &properties);
|
|
+
|
|
+ VkPhysicalDeviceIDProperties idProperties {};
|
|
+ idProperties.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES;
|
|
+ VkPhysicalDeviceProperties2 properties2 {};
|
|
+ properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
|
|
+ properties2.pNext = &idProperties;
|
|
+ if (DynamicVK::vkGetPhysicalDeviceProperties2) {
|
|
+ DynamicVK::vkGetPhysicalDeviceProperties2(device, &properties2);
|
|
+ }
|
|
|
|
- if (VK_API_VERSION_VARIANT(props.apiVersion) != 0 || props.apiVersion < VK_API_VERSION_1_3) {
|
|
- // unsupported device
|
|
+ std::string line = "physical name=\"" + std::string(properties.deviceName) + "\""
|
|
+ + " type=" + deviceTypeName(properties.deviceType)
|
|
+ + " vendorID=0x" + hexValue(properties.vendorID, 4)
|
|
+ + " driverVersion=0x" + hexValue(properties.driverVersion, 8)
|
|
+ + " apiVersion=" + std::to_string(VK_API_VERSION_MAJOR(properties.apiVersion)) + "."
|
|
+ + std::to_string(VK_API_VERSION_MINOR(properties.apiVersion)) + "."
|
|
+ + std::to_string(VK_API_VERSION_PATCH(properties.apiVersion))
|
|
+ + " deviceUUID=" + uuidToString(idProperties.deviceUUID)
|
|
+ + " driverUUID=" + uuidToString(idProperties.driverUUID);
|
|
+
|
|
+ if (VK_API_VERSION_VARIANT(properties.apiVersion) != 0 || properties.apiVersion < VK_API_VERSION_1_3) {
|
|
+ appendDeviceLog(line + " supported=no reason=apiVersion");
|
|
continue;
|
|
}
|
|
|
|
@@ -67,12 +237,105 @@ void Indium::initGlobalDeviceList() {
|
|
DynamicVK::vkGetPhysicalDeviceFeatures2(device, &features);
|
|
|
|
if (!features12.timelineSemaphore) {
|
|
- // unsupported device
|
|
+ appendDeviceLog(line + " supported=no reason=timelineSemaphore");
|
|
continue;
|
|
}
|
|
|
|
- globalDeviceList.push_back(std::make_shared<PrivateDevice>(std::move(device)));
|
|
+ appendDeviceLog(line + " supported=yes");
|
|
+ DeviceCandidate candidate {};
|
|
+ candidate.device = device;
|
|
+ candidate.properties = properties;
|
|
+ memcpy(candidate.deviceUUID, idProperties.deviceUUID, 16);
|
|
+ memcpy(candidate.driverUUID, idProperties.driverUUID, 16);
|
|
+ candidates.push_back(candidate);
|
|
+ }
|
|
+
|
|
+ // Resolve the explicit selection against the supported list. An explicit choice
|
|
+ // is honoured even when it names a software (CPU) device, because the user asked
|
|
+ // for it; the skip-CPU rule only affects the automatic default.
|
|
+ long selectedIndex = -1;
|
|
+ std::string selectedReason;
|
|
+
|
|
+ if (!selector.source.empty()) {
|
|
+ if (isAllDigits(selector.value)) {
|
|
+ long index = strtol(selector.value.c_str(), nullptr, 10);
|
|
+ if (index >= 0 && index < (long) candidates.size()) {
|
|
+ selectedIndex = index;
|
|
+ selectedReason = selector.source + " index " + selector.value;
|
|
+ }
|
|
+ }
|
|
+ if (selectedIndex < 0) {
|
|
+ for (size_t i = 0; i < candidates.size(); ++i) {
|
|
+ if (nameContains(candidates[i].properties.deviceName, selector.value)) {
|
|
+ selectedIndex = (long) i;
|
|
+ selectedReason = selector.source + " name~=\"" + selector.value + "\"";
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ if (selectedIndex < 0) {
|
|
+ appendDeviceLog("selector " + selector.source + " value=\"" + selector.value + "\" matched no supported device; falling back to the loader default");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ const bool haveHardwareDevice = std::any_of(candidates.begin(), candidates.end(), [](const DeviceCandidate& candidate) {
|
|
+ return candidate.properties.deviceType != VK_PHYSICAL_DEVICE_TYPE_CPU;
|
|
+ });
|
|
+
|
|
+ if (selectedIndex < 0) {
|
|
+ for (size_t i = 0; i < candidates.size(); ++i) {
|
|
+ if (skipCPUDevices && haveHardwareDevice && candidates[i].properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) {
|
|
+ continue;
|
|
+ }
|
|
+ selectedIndex = (long) i;
|
|
+ selectedReason = skipCPUDevices ? "loader-first-hardware" : "loader-first";
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (selectedIndex < 0) {
|
|
+ appendDeviceLog("no usable supported Vulkan device found; Metal will have no device");
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ DeviceCandidate& selected = candidates[selectedIndex];
|
|
+ appendDeviceLog("selected name=\"" + std::string(selected.properties.deviceName) + "\""
|
|
+ + " type=" + deviceTypeName(selected.properties.deviceType)
|
|
+ + " vendorID=0x" + hexValue(selected.properties.vendorID, 4)
|
|
+ + " deviceUUID=" + uuidToString(selected.deviceUUID)
|
|
+ + " reason=" + (selectedReason.empty() ? std::string("loader-first") : selectedReason));
|
|
+
|
|
+ VkPhysicalDeviceMemoryProperties selectedMemory {};
|
|
+ DynamicVK::vkGetPhysicalDeviceMemoryProperties(selected.device, &selectedMemory);
|
|
+ appendMemoryTable(selectedMemory);
|
|
+
|
|
+ // A VkDevice is created for the selected device only: its driver is initialized
|
|
+ // and, on a hybrid laptop, the discrete GPU is not woken for a device the
|
|
+ // rendering GPU cannot share memory with anyway. The old behaviour -- a VkDevice
|
|
+ // for every supported device, in the loader's order -- stays reachable with the
|
|
+ // opt-in guest file /tmp/metal-all-devices. MTLCopyAllDevices only ever exposes
|
|
+ // the default device, so nothing in the client depends on the full list.
|
|
+ std::vector<size_t> creationOrder;
|
|
+ creationOrder.push_back((size_t) selectedIndex);
|
|
+ if (allDevices) {
|
|
+ for (size_t i = 0; i < candidates.size(); ++i) {
|
|
+ if (i == (size_t) selectedIndex) {
|
|
+ continue;
|
|
+ }
|
|
+ if (skipCPUDevices && haveHardwareDevice && candidates[i].properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_CPU) {
|
|
+ continue;
|
|
+ }
|
|
+ creationOrder.push_back(i);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (size_t index: creationOrder) {
|
|
+ globalDeviceList.push_back(std::make_shared<PrivateDevice>(candidates[index].device));
|
|
}
|
|
+
|
|
+ appendDeviceLog("created VkDevice for " + std::to_string(globalDeviceList.size()) + " of "
|
|
+ + std::to_string(candidates.size()) + " supported device(s)"
|
|
+ + (allDevices ? " (allDevices)" : " (selected only)"));
|
|
};
|
|
|
|
void Indium::finitGlobalDeviceList() {
|
|
@@ -83,7 +346,8 @@ Indium::Device::~Device() {};
|
|
|
|
Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice):
|
|
_physicalDevice(physicalDevice)
|
|
-{
|
|
+{ Indium::SubmitThread::ensureStarted();
|
|
+
|
|
DynamicVK::vkGetPhysicalDeviceProperties(_physicalDevice, &_properties);
|
|
|
|
std::vector<VkQueueFamilyProperties> queueFamilies;
|
|
@@ -233,6 +497,36 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice):
|
|
}
|
|
}
|
|
|
|
+ // An unbound vertex-buffer slot is passed to vkCmdBindVertexBuffers as VK_NULL_HANDLE (see
|
|
+ // PrivateRenderCommandEncoder::updateBindings). Without nullDescriptor that is invalid and the GPU fetches from
|
|
+ // address 0: on RADV it faulted the gfx ring (page fault at 0x0) and reset the GPU. Enable only that feature, not the
|
|
+ // costlier robustBufferAccess2/robustImageAccess2.
|
|
+ bool robustness2Enabled = false;
|
|
+ VkPhysicalDeviceRobustness2FeaturesEXT robustness2 {};
|
|
+ robustness2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT;
|
|
+ {
|
|
+ bool hasRobustness2 = false;
|
|
+ for (const auto& prop: extProps) {
|
|
+ if (strcmp(prop.extensionName, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME) == 0) {
|
|
+ hasRobustness2 = true;
|
|
+ }
|
|
+ }
|
|
+ if (hasRobustness2) {
|
|
+ VkPhysicalDeviceFeatures2 query {};
|
|
+ query.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
|
|
+ query.pNext = &robustness2;
|
|
+ DynamicVK::vkGetPhysicalDeviceFeatures2(_physicalDevice, &query);
|
|
+ if (robustness2.nullDescriptor) {
|
|
+ robustness2.robustBufferAccess2 = VK_FALSE;
|
|
+ robustness2.robustImageAccess2 = VK_FALSE;
|
|
+ robustness2.pNext = features13.pNext;
|
|
+ features13.pNext = &robustness2;
|
|
+ extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
|
|
+ robustness2Enabled = true;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
VkDeviceCreateInfo deviceCreateInfo {};
|
|
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
|
|
deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data();
|
|
@@ -246,6 +540,7 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice):
|
|
}
|
|
|
|
_features = indiumFeatures;
|
|
+ _nullDescriptor = robustness2.nullDescriptor == VK_TRUE && robustness2Enabled;
|
|
|
|
for (const auto& index: queueFamilyIndices) {
|
|
VkQueue queue;
|
|
@@ -297,6 +592,9 @@ Indium::PrivateDevice::PrivateDevice(VkPhysicalDevice physicalDevice):
|
|
};
|
|
|
|
Indium::PrivateDevice::~PrivateDevice() {
|
|
+ for (const auto& entry : _renderPasses) DynamicVK::vkDestroyRenderPass(_device, entry.second, nullptr);
|
|
+ for (const auto& entry : _availableTimelineSemaphores) DynamicVK::vkDestroySemaphore(_device, entry.first, nullptr);
|
|
+ for (auto pool : _availableDescriptorPools) DynamicVK::vkDestroyDescriptorPool(_device, pool, nullptr);
|
|
if (_oneshotCommandPool) {
|
|
DynamicVK::vkDestroyCommandPool(_device, _oneshotCommandPool, nullptr);
|
|
}
|
|
@@ -308,6 +606,23 @@ std::string Indium::PrivateDevice::name() const {
|
|
return _properties.deviceName;
|
|
};
|
|
|
|
+std::pair<VkRenderPass, bool> Indium::PrivateDevice::acquireRenderPass(const VkRenderPassCreateInfo& info) {
|
|
+ auto key = renderPassKey(info);
|
|
+ std::unique_lock<std::mutex> lock(_renderPassMutex, std::defer_lock);
|
|
+ if (key) {
|
|
+ lock.lock();
|
|
+ auto found = _renderPasses.find(*key);
|
|
+ if (found != _renderPasses.end()) return {found->second, true};
|
|
+ // No eviction: submitted encoders and pipelines may still refer to a
|
|
+ // cached handle. Unusual descriptions beyond the bound remain private.
|
|
+ if (_renderPasses.size() >= 256) { key.reset(); lock.unlock(); }
|
|
+ }
|
|
+ VkRenderPass pass = VK_NULL_HANDLE;
|
|
+ if (DynamicVK::vkCreateRenderPass(_device, &info, nullptr, &pass) != VK_SUCCESS) abort();
|
|
+ if (key) _renderPasses.emplace(*key, pass);
|
|
+ return {pass, key.has_value()};
|
|
+}
|
|
+
|
|
std::shared_ptr<Indium::CommandQueue> Indium::PrivateDevice::newCommandQueue() {
|
|
return std::make_shared<PrivateCommandQueue>(shared_from_this());
|
|
};
|
|
@@ -317,9 +632,9 @@ std::shared_ptr<Indium::RenderPipelineState> Indium::PrivateDevice::newRenderPip
|
|
};
|
|
|
|
std::shared_ptr<Indium::ComputePipelineState> Indium::PrivateDevice::newComputePipelineState(const ComputePipelineDescriptor& descriptor, PipelineOption options, std::shared_ptr<ComputePipelineReflection> reflection) {
|
|
- if (options != PipelineOption::None) {
|
|
- throw std::runtime_error("TODO: support compute pipeline options");
|
|
- }
|
|
+ // Reflection is retained on the function; these flags do not change GPU code.
|
|
+ if ((static_cast<size_t>(options) & ~size_t(3)) != 0)
|
|
+ throw std::invalid_argument("Unsupported compute pipeline option");
|
|
return std::make_shared<PrivateComputePipelineState>(shared_from_this(), descriptor);
|
|
};
|
|
|
|
@@ -357,6 +672,7 @@ std::shared_ptr<Indium::Library> Indium::PrivateDevice::newLibrary(const void* d
|
|
break;
|
|
}
|
|
|
|
+ funcInfo.arguments = info.arguments;
|
|
funcInfo.bindings.insert(funcInfo.bindings.end(), info.bindings.begin(), info.bindings.end());
|
|
funcInfo.embeddedSamplers.insert(funcInfo.embeddedSamplers.end(), info.embeddedSamplers.begin(), info.embeddedSamplers.end());
|
|
}
|
|
@@ -480,9 +796,18 @@ void Indium::PrivateDevice::waitForSemaphore(VkSemaphore semaphore, uint64_t tar
|
|
wakeupEventLoop();
|
|
};
|
|
|
|
-// TODO: create a semaphore pool to avoid constantly creating and destroying semaphores
|
|
+// Non-exported timeline handles are exclusively leased until the last owner
|
|
+// (including GPU-completion callbacks) releases them. Preserve monotonic values.
|
|
|
|
Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() {
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_timelineSemaphorePoolMutex);
|
|
+ if (!_availableTimelineSemaphores.empty()) {
|
|
+ const auto entry = _availableTimelineSemaphores.back();
|
|
+ _availableTimelineSemaphores.pop_back();
|
|
+ return TimelineSemaphore { shared_from_this(), entry.first, entry.second };
|
|
+ }
|
|
+ }
|
|
VkSemaphoreTypeCreateInfo typeInfo {};
|
|
typeInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO;
|
|
typeInfo.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
|
|
@@ -505,7 +830,14 @@ Indium::TimelineSemaphore Indium::PrivateDevice::getTimelineSemaphore() {
|
|
};
|
|
|
|
void Indium::PrivateDevice::putTimelineSemaphore(const TimelineSemaphore& semaphore) {
|
|
- DynamicVK::vkDestroySemaphore(_device, semaphore.semaphore, nullptr);
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_timelineSemaphorePoolMutex);
|
|
+ if (_availableTimelineSemaphores.size() < 256 && semaphore.count != UINT64_MAX) {
|
|
+ _availableTimelineSemaphores.emplace_back(semaphore.semaphore, semaphore.count);
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ DynamicVK::vkDestroySemaphore(_device, semaphore.semaphore, nullptr);
|
|
};
|
|
|
|
std::shared_ptr<Indium::TimelineSemaphore> Indium::PrivateDevice::getWrappedTimelineSemaphore() {
|
|
@@ -551,3 +883,160 @@ std::shared_ptr<Indium::BinarySemaphore> Indium::PrivateDevice::getWrappedBinary
|
|
delete ptr;
|
|
});
|
|
};
|
|
+
|
|
+bool Indium::PrivateDevice::supportsDepth24Stencil8PixelFormat() {
|
|
+ VkFormatProperties properties {};
|
|
+ DynamicVK::vkGetPhysicalDeviceFormatProperties(_physicalDevice, VK_FORMAT_D24_UNORM_S8_UINT, &properties);
|
|
+ return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0;
|
|
+}
|
|
+
|
|
+uint64_t Indium::PrivateDevice::fallbackBufferAddress() {
|
|
+ std::scoped_lock lock(_fallbackMutex);
|
|
+ if (!_fallbackBuffer) {
|
|
+ std::vector<char> zeros(64 * 1024, 0);
|
|
+ _fallbackBuffer = newBuffer(zeros.data(), zeros.size(), ResourceOptions::StorageModeShared);
|
|
+ }
|
|
+ return _fallbackBuffer->gpuAddress();
|
|
+};
|
|
+
|
|
+std::shared_ptr<Indium::SamplerState> Indium::PrivateDevice::fallbackSampler() {
|
|
+ {
|
|
+ std::scoped_lock lock(_fallbackMutex);
|
|
+ if (_fallbackSampler) {
|
|
+ return _fallbackSampler;
|
|
+ }
|
|
+ }
|
|
+ auto sampler = newSamplerState(SamplerDescriptor {});
|
|
+ std::scoped_lock lock(_fallbackMutex);
|
|
+ if (!_fallbackSampler) {
|
|
+ _fallbackSampler = sampler;
|
|
+ }
|
|
+ return _fallbackSampler;
|
|
+};
|
|
+
|
|
+std::shared_ptr<Indium::Buffer> Indium::PrivateDevice::acquireScratchChunk() {
|
|
+ std::shared_ptr<Buffer> base;
|
|
+
|
|
+ {
|
|
+ std::scoped_lock lock(_scratchMutex);
|
|
+ if (!_scratchFree.empty()) {
|
|
+ base = std::move(_scratchFree.back());
|
|
+ _scratchFree.pop_back();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (!base) {
|
|
+ base = newBuffer(scratchChunkSize, ResourceOptions::StorageModeShared);
|
|
+ }
|
|
+
|
|
+ // the returned handle gives the chunk back instead of destroying it
|
|
+ auto self = shared_from_this();
|
|
+ return std::shared_ptr<Buffer>(base.get(), [self, base](Buffer*) {
|
|
+ std::scoped_lock lock(self->_scratchMutex);
|
|
+ self->_scratchFree.push_back(base);
|
|
+ });
|
|
+};
|
|
+
|
|
+VkDescriptorPool Indium::PrivateDevice::acquireDescriptorPool() {
|
|
+ VkDescriptorPool pool = VK_NULL_HANDLE;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_descriptorPoolMutex);
|
|
+ if (!_availableDescriptorPools.empty()) {
|
|
+ pool = _availableDescriptorPools.back();
|
|
+ _availableDescriptorPools.pop_back();
|
|
+ }
|
|
+ }
|
|
+ // Returned only after its encoder and GPU completion owner are released.
|
|
+ // Each acquisition has exclusive ownership; reset retains driver storage.
|
|
+ if (pool) {
|
|
+ if (DynamicVK::vkResetDescriptorPool(_device, pool, 0) != VK_SUCCESS) abort();
|
|
+ return pool;
|
|
+ }
|
|
+ VkDescriptorPoolCreateInfo info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
+ info.poolSizeCount = poolSizes.size();
|
|
+ info.pPoolSizes = poolSizes.data();
|
|
+ info.maxSets = 64;
|
|
+ if (DynamicVK::vkCreateDescriptorPool(_device, &info, nullptr, &pool) != VK_SUCCESS) abort();
|
|
+ return pool;
|
|
+}
|
|
+void Indium::PrivateDevice::releaseDescriptorPool(VkDescriptorPool pool) {
|
|
+ if (!pool) return;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_descriptorPoolMutex);
|
|
+ if (_availableDescriptorPools.size() < 128) {
|
|
+ _availableDescriptorPools.push_back(pool);
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ DynamicVK::vkDestroyDescriptorPool(_device, pool, nullptr);
|
|
+}
|
|
+
|
|
+// The Vulkan spec orders memory types so that a type whose flags are a subset of
|
|
+// another's comes first. A flag-less type (system RAM the GPU reads over PCIe,
|
|
+// which NVIDIA exposes) therefore precedes every DEVICE_LOCAL type, and the first
|
|
+// type allowed by memoryTypeBits put resources only the GPU touches in system RAM
|
|
+// there. Private and Memoryless resources now take the first allowed DEVICE_LOCAL
|
|
+// type and fall back to that first fit. Shared and Managed keep the first fit
|
|
+// that is host visible (and coherent for Shared): moving them to DEVICE_LOCAL and
|
|
+// HOST_VISIBLE BAR memory would make their CPU reads uncached. On RADV type 0 is
|
|
+// DEVICE_LOCAL, so nothing changes there.
|
|
+size_t Indium::PrivateDevice::findMemoryType(uint32_t memoryTypeBits, StorageMode storageMode, bool image) {
|
|
+ const auto& memory = _memoryProperties;
|
|
+ const bool gpuOnly = storageMode == StorageMode::Private || storageMode == StorageMode::Memoryless;
|
|
+ size_t firstFit = SIZE_MAX;
|
|
+ size_t deviceLocal = SIZE_MAX;
|
|
+
|
|
+ for (size_t i = 0; i < memory.memoryTypeCount; ++i) {
|
|
+ const auto& type = memory.memoryTypes[i];
|
|
+
|
|
+ if ((memoryTypeBits & (1u << i)) == 0) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if ((storageMode == StorageMode::Managed || storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) {
|
|
+ continue;
|
|
+ }
|
|
+
|
|
+ if (firstFit == SIZE_MAX) {
|
|
+ firstFit = i;
|
|
+ }
|
|
+
|
|
+ if (!gpuOnly || (type.propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0) {
|
|
+ deviceLocal = i;
|
|
+ break;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ const size_t chosen = deviceLocal != SIZE_MAX ? deviceLocal : firstFit;
|
|
+ if (chosen == SIZE_MAX) {
|
|
+ return chosen;
|
|
+ }
|
|
+
|
|
+ // Logged the first time each (kind, storage mode, type) occurs in the process;
|
|
+ // afterwards this is one relaxed load. 2 kinds x 4 modes x 32 types = 256 bits.
|
|
+ static std::atomic<uint64_t> logged[4] {};
|
|
+ const size_t bit = ((image ? 4u : 0u) + (static_cast<size_t>(storageMode) & 3)) * 32 + chosen;
|
|
+ const uint64_t mask = uint64_t(1) << (bit & 63);
|
|
+ if ((logged[bit >> 6].load(std::memory_order_relaxed) & mask) == 0 && (logged[bit >> 6].fetch_or(mask) & mask) == 0) {
|
|
+ static const char* modeNames[] = { "shared", "managed", "private", "memoryless" };
|
|
+ auto describe = [&](size_t index) {
|
|
+ const auto& type = memory.memoryTypes[index];
|
|
+ return std::to_string(index) + " flags=" + memoryFlagsString(type.propertyFlags)
|
|
+ + " heap=" + std::to_string(type.heapIndex)
|
|
+ + " heapDeviceLocal=" + ((memory.memoryHeaps[type.heapIndex].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) ? "yes" : "no");
|
|
+ };
|
|
+ appendDeviceLog(std::string("memory alloc kind=") + (image ? "texture" : "buffer")
|
|
+ + " storage=" + modeNames[static_cast<size_t>(storageMode) & 3]
|
|
+ + " typeBits=0x" + hexValue(memoryTypeBits, 8)
|
|
+ + " type=" + describe(chosen)
|
|
+ + " firstFit=" + describe(firstFit)
|
|
+ + (chosen != firstFit ? " changed=yes" : " changed=no"));
|
|
+ }
|
|
+
|
|
+ return chosen;
|
|
+};
|
|
diff --git a/src/indium/dynamic-vk.cpp b/src/indium/dynamic-vk.cpp
|
|
index a4677c3..69ef23f 100644
|
|
--- a/src/indium/dynamic-vk.cpp
|
|
+++ b/src/indium/dynamic-vk.cpp
|
|
@@ -1,4 +1,5 @@
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <cstdlib>
|
|
|
|
#ifdef DARLING
|
|
#include <elfcalls.h>
|
|
@@ -6,6 +7,10 @@
|
|
#endif
|
|
|
|
static void* libraryHandle = NULL;
|
|
+static void (*captureBegin)(void*) = nullptr;
|
|
+static void (*captureEnd)(int) = nullptr;
|
|
+void Indium::DynamicVK::beginDiagnosticCapture(VkInstance instance) { if (captureBegin) captureBegin(instance); }
|
|
+void Indium::DynamicVK::endDiagnosticCapture(bool presented) { if (captureEnd) captureEnd(presented); }
|
|
PFN_vkGetInstanceProcAddr Indium::DynamicVK::vkGetInstanceProcAddr = NULL;
|
|
PFN_vkCreateInstance Indium::DynamicVK::vkCreateInstance = NULL;
|
|
PFN_vkEnumerateInstanceLayerProperties Indium::DynamicVK::vkEnumerateInstanceLayerProperties = NULL;
|
|
@@ -32,6 +37,16 @@ static Indium::DynamicVK::DynamicFunctionBase* const eagerlyResolvedFunctions[]
|
|
#endif
|
|
|
|
bool Indium::DynamicVK::init() {
|
|
+#ifdef DARLING
|
|
+ const char* capture = getenv("RCLOL_RENDERDOC_CAPTURE");
|
|
+ if (capture && capture[0] == '1') {
|
|
+ void* bridge = _elfcalls->dlopen("@PROJECT_ROOT@/build/renderdoc/capture-bridge.so");
|
|
+ if (bridge) {
|
|
+ captureBegin = reinterpret_cast<void(*)(void*)>(DLSYM(bridge, "riot_capture_begin"));
|
|
+ captureEnd = reinterpret_cast<void(*)(int)>(DLSYM(bridge, "riot_capture_end"));
|
|
+ }
|
|
+ }
|
|
+#endif
|
|
#ifdef DARLING
|
|
libraryHandle = _elfcalls->dlopen("libvulkan.so.1");
|
|
#else
|
|
diff --git a/src/indium/indium.cpp b/src/indium/indium.cpp
|
|
index e7c06fe..6e1f4b1 100644
|
|
--- a/src/indium/indium.cpp
|
|
+++ b/src/indium/indium.cpp
|
|
@@ -10,15 +10,26 @@
|
|
#include <string_view>
|
|
|
|
#include <iostream>
|
|
+#include <cstdio>
|
|
+#include <mutex>
|
|
+#include <unistd.h>
|
|
|
|
VkInstance Indium::globalInstance = VK_NULL_HANDLE;
|
|
|
|
static VkDebugUtilsMessengerEXT debugMessenger = VK_NULL_HANDLE;
|
|
|
|
static volatile int noOptOut;
|
|
+static FILE* validationLog = nullptr;
|
|
+static std::mutex validationLogMutex;
|
|
|
|
static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type, const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData) {
|
|
- std::cerr << "Validation message: " << callbackData->pMessage << std::endl;
|
|
+ if (validationLog) {
|
|
+ std::lock_guard<std::mutex> lock(validationLogMutex);
|
|
+ fprintf(validationLog, "pid=%d severity=%u id=%s: %s\n", getpid(), unsigned(severity), callbackData->pMessageIdName ? callbackData->pMessageIdName : "", callbackData->pMessage);
|
|
+ fflush(validationLog);
|
|
+ } else {
|
|
+ std::cerr << "Validation message: " << callbackData->pMessage << std::endl;
|
|
+ }
|
|
|
|
if ((severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT) != 0) {
|
|
// convenient breakpoint
|
|
@@ -43,6 +54,11 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC
|
|
VK_KHR_SURFACE_EXTENSION_NAME,
|
|
};
|
|
|
|
+ if (access("/tmp/metal-validation", F_OK) == 0) {
|
|
+ enableValidation = true;
|
|
+ validationLog = fopen("/tmp/metal-validation.log", "a");
|
|
+ }
|
|
+
|
|
uint32_t count;
|
|
|
|
std::vector<VkLayerProperties> layerProps;
|
|
@@ -97,13 +113,13 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC
|
|
|
|
VkValidationFeaturesEXT valFeat {};
|
|
std::vector<VkValidationFeatureEnableEXT> valFeatures {
|
|
- VK_VALIDATION_FEATURE_ENABLE_DEBUG_PRINTF_EXT,
|
|
+ VK_VALIDATION_FEATURE_ENABLE_SYNCHRONIZATION_VALIDATION_EXT,
|
|
};
|
|
valFeat.sType = VK_STRUCTURE_TYPE_VALIDATION_FEATURES_EXT;
|
|
valFeat.enabledValidationFeatureCount = valFeatures.size();
|
|
valFeat.pEnabledValidationFeatures = valFeatures.data();
|
|
|
|
- if (enableValidation) {
|
|
+ if (enableValidation && access("/tmp/metal-validation-sync", F_OK) == 0) {
|
|
createInfo.pNext = &valFeat;
|
|
}
|
|
|
|
@@ -133,7 +149,10 @@ void Indium::init(const char** additionalExtensions, size_t additionalExtensionC
|
|
initGlobalDeviceList();
|
|
};
|
|
|
|
+namespace Indium { void finishCommandRetirement(); }
|
|
+
|
|
void Indium::finit() {
|
|
+ finishCommandRetirement();
|
|
finitGlobalDeviceList();
|
|
|
|
if (debugMessenger && DynamicVK::vkDestroyDebugUtilsMessengerEXT) {
|
|
@@ -146,4 +165,5 @@ void Indium::finit() {
|
|
DynamicVK::finit();
|
|
|
|
Iridium::finit();
|
|
+ if (validationLog) { fclose(validationLog); validationLog = nullptr; }
|
|
};
|
|
diff --git a/src/indium/library.cpp b/src/indium/library.cpp
|
|
index d56a213..66a6019 100644
|
|
--- a/src/indium/library.cpp
|
|
+++ b/src/indium/library.cpp
|
|
@@ -1,7 +1,13 @@
|
|
+#include <indium/gpu-timing.private.hpp>
|
|
+#include <indium/outline-chain.private.hpp>
|
|
#include <indium/library.private.hpp>
|
|
#include <indium/device.private.hpp>
|
|
#include <indium/sampler.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/counters.private.hpp>
|
|
+#include <cstdio>
|
|
+#include <sys/stat.h>
|
|
+#include <unistd.h>
|
|
|
|
Indium::Function::~Function() {};
|
|
Indium::Library::~Library() {};
|
|
@@ -26,6 +32,8 @@ Indium::PrivateLibrary::PrivateLibrary(std::shared_ptr<PrivateDevice> device, co
|
|
_privateDevice(device),
|
|
_functionInfos(functionInfos)
|
|
{
|
|
+ GpuTiming::noteLibrary(this, data, dataLength);
|
|
+ OutlineChain::noteLibrary(this, data, dataLength);
|
|
VkShaderModuleCreateInfo createInfo {};
|
|
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
|
|
createInfo.codeSize = dataLength;
|
|
@@ -36,6 +44,20 @@ Indium::PrivateLibrary::PrivateLibrary(std::shared_ptr<PrivateDevice> device, co
|
|
abort();
|
|
}
|
|
|
|
+ // Diagnostic: with /tmp/metal-debug-spirv present, keep the first translated modules for offline inspection
|
|
+ if (Indium::spirvDebugEnabled()) {
|
|
+ static unsigned dumped = 0;
|
|
+ if (dumped++ < 8192) {
|
|
+ mkdir("/tmp/spv", 0777);
|
|
+ char path[96];
|
|
+ snprintf(path, sizeof(path), "/tmp/spv/lib-%p.spv", (void*) this);
|
|
+ if (FILE* file = fopen(path, "wb")) {
|
|
+ fwrite(data, 1, dataLength, file);
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
// create sampler states for embedded samplers
|
|
for (auto& [name, funcInfo]: _functionInfos) {
|
|
for (const auto& embeddedSampler: funcInfo.embeddedSamplers) {
|
|
@@ -146,3 +168,5 @@ Indium::PrivateLibrary::~PrivateLibrary() {
|
|
std::shared_ptr<Indium::Function> Indium::PrivateLibrary::newFunction(const std::string& name) {
|
|
return std::make_shared<PrivateFunction>(shared_from_this(), name, _functionInfos[name]);
|
|
};
|
|
+
|
|
+const std::vector<Iridium::ArgumentInfo>& Indium::PrivateFunction::argumentInfo() const { return _functionInfo.arguments; }
|
|
diff --git a/src/indium/render-command-encoder.cpp b/src/indium/render-command-encoder.cpp
|
|
index b27ffe5..dd7434d 100644
|
|
--- a/src/indium/render-command-encoder.cpp
|
|
+++ b/src/indium/render-command-encoder.cpp
|
|
@@ -1,3 +1,8 @@
|
|
+#include <atomic>
|
|
+#include <indium/outline-chain.private.hpp>
|
|
+#include <cstdarg>
|
|
+#include <unistd.h>
|
|
+#include <cstdio>
|
|
#include <indium/render-command-encoder.private.hpp>
|
|
#include <indium/command-buffer.private.hpp>
|
|
#include <indium/render-pass.hpp>
|
|
@@ -12,35 +17,120 @@
|
|
#include <indium/depth-stencil.private.hpp>
|
|
#include <indium/command-encoder.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/counters.private.hpp>
|
|
+#include <indium/gpu-timing.private.hpp>
|
|
#include <vulkan/vulkan_core.h>
|
|
+#include <time.h>
|
|
+namespace {
|
|
+// diagnostic (guest flags /tmp/metal-gpu-timing and, to start logging, /tmp/metal-seqlog): viewport, scissor, draw sizes and
|
|
+// small constant uploads, interleaved with the pass log in /tmp/indium-gpu-seq.log
|
|
+static bool seqLogActive() {
|
|
+ static unsigned calls = 0;
|
|
+ static bool active = false;
|
|
+ if ((calls++ & 255) == 0) active = access("/tmp/metal-seqlog", F_OK) == 0;
|
|
+ return active;
|
|
+}
|
|
+static void seqLog(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
|
+static void seqLog(const char* fmt, ...) {
|
|
+ static FILE* file = fopen("/tmp/indium-gpu-seq.log", "a");
|
|
+ if (!file || !seqLogActive()) return;
|
|
+ va_list args;
|
|
+ va_start(args, fmt);
|
|
+ vfprintf(file, fmt, args);
|
|
+ va_end(args);
|
|
+ fflush(file);
|
|
+}
|
|
+static void seqLogBytes(const char* stage, const void* bytes, size_t length, size_t index) {
|
|
+ if (length > 96 || !bytes) return;
|
|
+ char buf[512];
|
|
+ int n = snprintf(buf, sizeof(buf), " %s%zu len=%zu f=", stage, index, length);
|
|
+ const float* f = static_cast<const float*>(bytes);
|
|
+ for (size_t i = 0; i + 4 <= length && n < 480; i += 4) n += snprintf(buf + n, sizeof(buf) - n, "%.3g ", f[i / 4]);
|
|
+ seqLog("%s\n", buf);
|
|
+}
|
|
+}
|
|
+
|
|
+
|
|
+
|
|
+namespace {
|
|
+ // Diagnostic (guest /tmp flag files, re-read twice a second): skip every draw of render passes by attachment size,
|
|
+ // to measure what each pass costs on the GPU. Not for play.
|
|
+ struct DrawSkip { const void* encoder = nullptr; bool skip = false; };
|
|
+ thread_local DrawSkip drawSkip;
|
|
+
|
|
+ // Diagnostic flag files, refreshed every 2048 passes (a clock_gettime per pass was a syscall in the render hot path).
|
|
+ bool shouldSkipPass(size_t width, size_t height, bool drawableFormat) {
|
|
+ static unsigned counter = 0;
|
|
+ static bool f1180, f512, f1024, f1080, f242, fui, fscene;
|
|
+ if ((counter++ & 2047) == 0) {
|
|
+ f1180 = access("/tmp/metal-skip-1180", F_OK) == 0;
|
|
+ f512 = access("/tmp/metal-skip-512", F_OK) == 0;
|
|
+ f1024 = access("/tmp/metal-skip-1024", F_OK) == 0;
|
|
+ f1080 = access("/tmp/metal-skip-1080", F_OK) == 0;
|
|
+ f242 = access("/tmp/metal-skip-242", F_OK) == 0;
|
|
+ fui = access("/tmp/metal-skip-ui", F_OK) == 0;
|
|
+ fscene = access("/tmp/metal-skip-scene", F_OK) == 0;
|
|
+ }
|
|
+ if (width == 1180 && height == 1184) return f1180;
|
|
+ if (width == 512 && height == 512) return f512;
|
|
+ if (width == 1024 && height == 1024) return f1024;
|
|
+ if (width == 1920 && height == 1080) return f1080 || (drawableFormat ? fui : fscene);
|
|
+ if (width == 242 && height == 242) return f242;
|
|
+ return false;
|
|
+ }
|
|
+}
|
|
|
|
Indium::RenderCommandEncoder::~RenderCommandEncoder() {};
|
|
|
|
-Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer, const RenderPassDescriptor& descriptor):
|
|
+Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr<PrivateCommandBuffer> commandBuffer, const RenderPassDescriptor& inputDescriptor):
|
|
_privateCommandBuffer(commandBuffer),
|
|
- _descriptor(descriptor),
|
|
+ _descriptor(inputDescriptor),
|
|
_privateDevice(commandBuffer->privateDevice())
|
|
{
|
|
+ auto selectSubresource = [](RenderPassAttachmentDescriptor& attachment) {
|
|
+ if (!attachment.texture) return;
|
|
+ auto texture = std::dynamic_pointer_cast<PrivateTexture>(attachment.texture);
|
|
+ if (texture->textureType() != TextureType::e2D && texture->textureType() != TextureType::e2DArray) return;
|
|
+ if (!attachment.level && !attachment.slice && texture->mipmapLevelCount() == 1 && texture->arrayLength() == 1) return;
|
|
+ if (attachment.level >= texture->mipmapLevelCount() || attachment.slice >= texture->arrayLength())
|
|
+ throw std::invalid_argument("Render attachment subresource out of range");
|
|
+ attachment.texture = texture->newTextureView(texture->pixelFormat(), TextureType::e2D,
|
|
+ Range<size_t>{attachment.level, 1}, Range<size_t>{attachment.slice, 1});
|
|
+ attachment.level = 0;
|
|
+ attachment.slice = 0;
|
|
+ };
|
|
+ for (auto& color : _descriptor.colorAttachments) selectSubresource(color);
|
|
+ if (_descriptor.depthAttachment) selectSubresource(*_descriptor.depthAttachment);
|
|
+ if (_descriptor.stencilAttachment) selectSubresource(*_descriptor.stencilAttachment);
|
|
+ const auto& descriptor = _descriptor;
|
|
auto buf = _privateCommandBuffer.lock();
|
|
|
|
auto vkDevice = _privateDevice->device();
|
|
auto vkCmdBuf = buf->commandBuffer();
|
|
|
|
- VkDescriptorPoolCreateInfo poolCreateInfo {};
|
|
- poolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
|
|
- poolCreateInfo.poolSizeCount = poolSizes.size();
|
|
- poolCreateInfo.pPoolSizes = poolSizes.data();
|
|
- poolCreateInfo.maxSets = 64; // i guess
|
|
-
|
|
- if (DynamicVK::vkCreateDescriptorPool(vkDevice, &poolCreateInfo, nullptr, &_pool) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
+ std::shared_ptr<Texture> firstTexture;
|
|
+ for (const auto& color: descriptor.colorAttachments) {
|
|
+ if (color.texture) { firstTexture = color.texture; break; }
|
|
}
|
|
-
|
|
- auto firstTexture = descriptor.colorAttachments.front().texture;
|
|
+ if (!firstTexture) firstTexture = descriptor.depthAttachment ? descriptor.depthAttachment->texture :
|
|
+ (descriptor.stencilAttachment ? descriptor.stencilAttachment->texture : nullptr);
|
|
+ if (!firstTexture) throw std::invalid_argument("Render pass requires an attachment");
|
|
+ drawSkip.encoder = this;
|
|
+ drawSkip.skip = shouldSkipPass(firstTexture->width(), firstTexture->height(), firstTexture->pixelFormat() == PixelFormat::RGBA8Unorm);
|
|
+ // Depth and stencil may be given as different texture objects (e.g. a view of a combined format) that share one image
|
|
+ if (descriptor.depthAttachment && descriptor.stencilAttachment &&
|
|
+ descriptor.depthAttachment->texture != descriptor.stencilAttachment->texture) {
|
|
+ auto depthImage = std::dynamic_pointer_cast<PrivateTexture>(descriptor.depthAttachment->texture);
|
|
+ auto stencilImage = std::dynamic_pointer_cast<PrivateTexture>(descriptor.stencilAttachment->texture);
|
|
+ if (!depthImage || !stencilImage || depthImage->image() != stencilImage->image())
|
|
+ throw std::invalid_argument("Separate depth and stencil textures are not supported");
|
|
+ }
|
|
+ auto depthStencilTexture = descriptor.depthAttachment ? descriptor.depthAttachment->texture :
|
|
+ (descriptor.stencilAttachment ? descriptor.stencilAttachment->texture : nullptr);
|
|
std::vector<VkClearValue> clearValues;
|
|
|
|
for (const auto& color: descriptor.colorAttachments) {
|
|
+ if (!color.texture) continue;
|
|
auto clearColor = color.clearColor;
|
|
VkClearValue clearValue {};
|
|
clearValue.color.float32[0] = clearColor.red;
|
|
@@ -65,36 +155,59 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr
|
|
std::vector<VkSubpassDependency> dependencies;
|
|
|
|
for (const auto& color: descriptor.colorAttachments) {
|
|
+ if (!color.texture) continue;
|
|
auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(color.texture);
|
|
VkAttachmentDescription desc {};
|
|
desc.format = pixelFormatToVkFormat(color.texture->pixelFormat());
|
|
desc.samples = VK_SAMPLE_COUNT_1_BIT; // TODO: support multisampling
|
|
- desc.loadOp = loadActionToVkAttachmentLoadOp(color.loadAction, true);
|
|
- desc.storeOp = storeActionToVkAttachmentStoreOp(color.storeAction, true);
|
|
+ // Desktop Metal leaves an attachment's memory alone for DontCare, and the game depends on it. In Vulkan,
|
|
+ // DONT_CARE plus an UNDEFINED initial layout lets the driver throw the contents away (RADV re-initialises the
|
|
+ // image's compression metadata), so only an explicit Clear may discard; everything else is preserved.
|
|
+ const bool colorClear = color.loadAction == LoadAction::Clear;
|
|
+ desc.loadOp = colorClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
+ // always STORE: free on this kind of GPU, and DontCare/Unknown/resolve actions must not discard either
|
|
+ desc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
desc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
- desc.initialLayout = (color.loadAction == LoadAction::Load) ? privateTexture->imageLayout() : VK_IMAGE_LAYOUT_UNDEFINED;
|
|
+ desc.initialLayout = colorClear ? VK_IMAGE_LAYOUT_UNDEFINED : privateTexture->imageLayout();
|
|
desc.finalLayout = privateTexture->imageLayout();
|
|
renderPassAttachments.push_back(desc);
|
|
}
|
|
|
|
- if (descriptor.depthAttachment) {
|
|
- auto privateTexture = std::dynamic_pointer_cast<PrivateTexture>(descriptor.depthAttachment->texture);
|
|
- VkAttachmentDescription desc {};
|
|
- desc.format = pixelFormatToVkFormat(privateTexture->pixelFormat());
|
|
- desc.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
- desc.loadOp = loadActionToVkAttachmentLoadOp(descriptor.depthAttachment->loadAction, false);
|
|
- desc.storeOp = storeActionToVkAttachmentStoreOp(descriptor.depthAttachment->storeAction, false);
|
|
- desc.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
- desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
- desc.initialLayout = (descriptor.depthAttachment->loadAction == LoadAction::Load) ? privateTexture->imageLayout() : VK_IMAGE_LAYOUT_UNDEFINED;
|
|
- desc.finalLayout = privateTexture->imageLayout();
|
|
- renderPassAttachments.push_back(desc);
|
|
- }
|
|
|
|
- if (descriptor.stencilAttachment) {
|
|
- throw std::runtime_error("TODO: support stencil attachments");
|
|
- }
|
|
+ if (depthStencilTexture) {
|
|
+ auto texture = std::dynamic_pointer_cast<PrivateTexture>(depthStencilTexture);
|
|
+ VkAttachmentDescription desc {};
|
|
+ desc.format = pixelFormatToVkFormat(texture->pixelFormat());
|
|
+ desc.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
+ // Same rule as for colour (see above): the game binds the scene depth buffer to a run of effect passes with
|
|
+ // DontCare/DontCare and then resumes the scene with Load. With UNDEFINED/DONT_CARE the depth buffer came back
|
|
+ // as garbage in tiles, which is what punched black blocks into everything drawn afterwards. An aspect the
|
|
+ // pass does not attach is preserved too. Default for depth and stencil is Clear.
|
|
+ auto isClear = [](const auto& attachment) {
|
|
+ return attachment && (attachment->loadAction == LoadAction::Clear || attachment->loadAction == LoadAction::Default);
|
|
+ };
|
|
+ const bool depthClear = isClear(descriptor.depthAttachment);
|
|
+ const bool stencilClear = isClear(descriptor.stencilAttachment);
|
|
+ desc.loadOp = depthClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
+ desc.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
+ desc.stencilLoadOp = stencilClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
|
|
+ desc.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
+ desc.initialLayout = (depthClear && stencilClear) ? VK_IMAGE_LAYOUT_UNDEFINED : texture->imageLayout();
|
|
+ desc.finalLayout = texture->imageLayout();
|
|
+ renderPassAttachments.push_back(desc);
|
|
+ _readWriteTextures.push_back(depthStencilTexture);
|
|
+ _depthRoot = texture;
|
|
+ while (auto parent = std::dynamic_pointer_cast<PrivateTexture>(_depthRoot->parentTexture())) _depthRoot = parent;
|
|
+ }
|
|
+ // League uses fragment-to-fragment barriers for texture/render-target scope.
|
|
+ VkSubpassDependency dependency {};
|
|
+ dependency.srcSubpass = dependency.dstSubpass = 0;
|
|
+ dependency.srcStageMask = dependency.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
+ dependency.srcAccessMask = dependency.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT |
|
|
+ VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
+ dependency.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;
|
|
+ dependencies.push_back(dependency);
|
|
|
|
std::vector<VkAttachmentReference> colorAttachments;
|
|
VkAttachmentReference depthStencilAttachment {};
|
|
@@ -102,13 +215,15 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr
|
|
size_t index = 0;
|
|
for (const auto& color: descriptor.colorAttachments) {
|
|
VkAttachmentReference ref {};
|
|
- ref.attachment = index;
|
|
- ref.layout = VK_IMAGE_LAYOUT_GENERAL;
|
|
+ ref.attachment = color.texture ? index : VK_ATTACHMENT_UNUSED;
|
|
+ // textures that rest outside GENERAL (see ConcreteTexture::imageLayout) are written as colour attachments
|
|
+ ref.layout = (color.texture && std::dynamic_pointer_cast<PrivateTexture>(color.texture)->imageLayout() != VK_IMAGE_LAYOUT_GENERAL) ?
|
|
+ VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_GENERAL;
|
|
colorAttachments.push_back(ref);
|
|
- ++index;
|
|
+ if (color.texture) ++index;
|
|
}
|
|
|
|
- if (descriptor.depthAttachment) {
|
|
+ if (depthStencilTexture) {
|
|
depthStencilAttachment.attachment = index;
|
|
depthStencilAttachment.layout = VK_IMAGE_LAYOUT_GENERAL;
|
|
++index;
|
|
@@ -129,19 +244,19 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr
|
|
renderPassInfo.dependencyCount = dependencies.size();
|
|
renderPassInfo.pDependencies = dependencies.data();
|
|
|
|
- if (DynamicVK::vkCreateRenderPass(vkDevice, &renderPassInfo, nullptr, &_renderPass) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
+ auto renderPassLease = _privateDevice->acquireRenderPass(renderPassInfo);
|
|
+ _renderPass = renderPassLease.first;
|
|
+ _renderPassDeviceOwned = renderPassLease.second;
|
|
|
|
std::vector<VkImageView> framebufferAttachments;
|
|
|
|
for (const auto& colorAttachment: descriptor.colorAttachments) {
|
|
+ if (!colorAttachment.texture) continue;
|
|
framebufferAttachments.push_back(std::dynamic_pointer_cast<PrivateTexture>(colorAttachment.texture)->imageView());
|
|
}
|
|
|
|
- if (descriptor.depthAttachment) {
|
|
- framebufferAttachments.push_back(std::dynamic_pointer_cast<PrivateTexture>(descriptor.depthAttachment->texture)->imageView());
|
|
+ if (depthStencilTexture) {
|
|
+ framebufferAttachments.push_back(std::dynamic_pointer_cast<PrivateTexture>(depthStencilTexture)->imageView());
|
|
}
|
|
|
|
VkFramebufferCreateInfo framebufferInfo {};
|
|
@@ -165,7 +280,51 @@ Indium::PrivateRenderCommandEncoder::PrivateRenderCommandEncoder(std::shared_ptr
|
|
renderPassBeginInfo.renderArea.extent.height = firstTexture->height();
|
|
renderPassBeginInfo.clearValueCount = clearValues.size();
|
|
renderPassBeginInfo.pClearValues = clearValues.data();
|
|
- DynamicVK::vkCmdBeginRenderPass(vkCmdBuf, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
|
+ if (GpuTiming::enabled()) {
|
|
+ std::string key = std::to_string(firstTexture->width()) + "x" + std::to_string(firstTexture->height());
|
|
+ int n = 0;
|
|
+ for (const auto& color: descriptor.colorAttachments) {
|
|
+ if (!color.texture) continue;
|
|
+ key += " c" + std::to_string(n++) + "=" + std::to_string((int) color.texture->pixelFormat()) + "/" + std::to_string((int) color.loadAction) + "/" + std::to_string((int) color.storeAction);
|
|
+ }
|
|
+ if (depthStencilTexture) {
|
|
+ key += " d=" + std::to_string((int) depthStencilTexture->pixelFormat());
|
|
+ if (descriptor.depthAttachment) key += "/" + std::to_string((int) descriptor.depthAttachment->loadAction) + "/" + std::to_string((int) descriptor.depthAttachment->storeAction);
|
|
+ }
|
|
+ GpuTiming::noteTarget(key);
|
|
+ if (seqLogActive()) {
|
|
+ std::string line = key;
|
|
+ for (const auto& color: descriptor.colorAttachments) {
|
|
+ char tmp[40];
|
|
+ if (color.texture) { snprintf(tmp, sizeof(tmp), " ct=%p", (void*) color.texture.get()); line += tmp; }
|
|
+ }
|
|
+ if (depthStencilTexture) { char tmp[40]; snprintf(tmp, sizeof(tmp), " dt=%p", (void*) depthStencilTexture.get()); line += tmp; }
|
|
+ seqLog("%s\n", line.c_str());
|
|
+ }
|
|
+ }
|
|
+ {
|
|
+ // A pass without a clear that never draws (the game's skipped outline passes, empty passes) does nothing in Metal
|
|
+ // either, but a begun-and-ended Vulkan render pass costs a pipeline flush. Begin it on the first draw instead.
|
|
+ // Live switch: guest file /tmp/metal-no-lazy-pass turns it off (re-read every 256 passes).
|
|
+ static std::atomic<bool> lazySetting {true};
|
|
+ static std::atomic<unsigned> lazyChecks {0};
|
|
+ if ((lazyChecks.fetch_add(1, std::memory_order_relaxed) & 255) == 0)
|
|
+ lazySetting.store(access("/tmp/metal-no-lazy-pass", F_OK) != 0, std::memory_order_relaxed);
|
|
+ const bool lazyAllowed = lazySetting.load(std::memory_order_relaxed);
|
|
+ bool hasClear = false;
|
|
+ for (const auto& attachment: renderPassAttachments) {
|
|
+ if (attachment.loadOp == VK_ATTACHMENT_LOAD_OP_CLEAR || attachment.stencilLoadOp == VK_ATTACHMENT_LOAD_OP_CLEAR) hasClear = true;
|
|
+ }
|
|
+ if (lazyAllowed && !hasClear) {
|
|
+ _lazyBeginInfo = renderPassBeginInfo;
|
|
+ _lazyBeginInfo.clearValueCount = 0;
|
|
+ _lazyBeginInfo.pClearValues = nullptr;
|
|
+ _passBegun = false;
|
|
+ } else {
|
|
+ GpuTiming::beginPass(vkDevice, vkCmdBuf, firstTexture->width(), firstTexture->height());
|
|
+ DynamicVK::vkCmdBeginRenderPass(vkCmdBuf, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
|
+ }
|
|
+ }
|
|
|
|
// set default values
|
|
|
|
@@ -190,16 +349,21 @@ Indium::PrivateRenderCommandEncoder::~PrivateRenderCommandEncoder() {
|
|
if (_framebuffer) {
|
|
DynamicVK::vkDestroyFramebuffer(_privateDevice->device(), _framebuffer, nullptr);
|
|
}
|
|
- if (_renderPass) {
|
|
+ if (_renderPass && !_renderPassDeviceOwned) {
|
|
DynamicVK::vkDestroyRenderPass(_privateDevice->device(), _renderPass, nullptr);
|
|
}
|
|
- DynamicVK::vkDestroyDescriptorPool(_privateDevice->device(), _pool, 0);
|
|
+ if (_pool) _privateDevice->releaseDescriptorPool(_pool);
|
|
+ for (auto pool : _retiredPools) _privateDevice->releaseDescriptorPool(pool);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setRenderPipelineState(std::shared_ptr<RenderPipelineState> renderPipelineState) {
|
|
- auto buf = _privateCommandBuffer.lock();
|
|
- _privatePSO = std::dynamic_pointer_cast<PrivateRenderPipelineState>(renderPipelineState);
|
|
- _privatePSO->recreatePipeline(_renderPass, false);
|
|
+void Indium::PrivateRenderCommandEncoder::setRenderPipelineState(const std::shared_ptr<RenderPipelineState>& renderPipelineState) {
|
|
+ if (_privatePSO == renderPipelineState) return;
|
|
+ // Keep each pipeline once, including all layouts and immutable samplers
|
|
+ // referenced by earlier draws, until this encoder retires.
|
|
+ auto [entry, inserted] = _pipelineOwners.acquire(renderPipelineState);
|
|
+ if (!entry || !entry->value) throw std::invalid_argument("Invalid render pipeline state");
|
|
+ _privatePSO = std::static_pointer_cast<PrivateRenderPipelineState>(entry->owner);
|
|
+ _privatePSO->recreatePipeline(_renderPass, false);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setFrontFacingWinding(Winding frontFaceWinding) {
|
|
@@ -225,6 +389,7 @@ void Indium::PrivateRenderCommandEncoder::setDepthClipMode(DepthClipMode depthCl
|
|
}
|
|
};
|
|
|
|
+
|
|
void Indium::PrivateRenderCommandEncoder::setViewport(const Viewport& viewport) {
|
|
setViewports(&viewport, 1);
|
|
};
|
|
@@ -239,7 +404,7 @@ void Indium::PrivateRenderCommandEncoder::setViewports(const Viewport* viewports
|
|
// so we use a Vulkan 1.1 feature here and just flip the Y-axis of the viewport
|
|
// (with a corresponding change in the origin).
|
|
vkViewport.x = viewport.originX;
|
|
- vkViewport.y = viewport.height - viewport.originY;
|
|
+ vkViewport.y = viewport.height + viewport.originY;
|
|
vkViewport.width = viewport.width;
|
|
vkViewport.height = -viewport.height;
|
|
// the documentation for setViewport and setViewports states that znear and zfar must be between
|
|
@@ -251,6 +416,7 @@ void Indium::PrivateRenderCommandEncoder::setViewports(const Viewport* viewports
|
|
vkViewport.maxDepth = std::clamp(viewport.zfar, 0., 1.);
|
|
tmp.push_back(vkViewport);
|
|
}
|
|
+ if (GpuTiming::enabled() && !tmp.empty()) seqLog(" VP %.0f %.0f %.0f %.0f\n", tmp[0].x, tmp[0].y, tmp[0].width, tmp[0].height);
|
|
DynamicVK::vkCmdSetViewportWithCount(buf->commandBuffer(), tmp.size(), tmp.data());
|
|
};
|
|
|
|
@@ -274,6 +440,7 @@ void Indium::PrivateRenderCommandEncoder::setScissorRects(const ScissorRect* sci
|
|
vkRect.extent.height = scissorRect.height;
|
|
tmp.push_back(vkRect);
|
|
}
|
|
+ if (GpuTiming::enabled() && !tmp.empty()) seqLog(" SC %d %d %u %u\n", tmp[0].offset.x, tmp[0].offset.y, tmp[0].extent.width, tmp[0].extent.height);
|
|
DynamicVK::vkCmdSetScissorWithCount(buf->commandBuffer(), tmp.size(), tmp.data());
|
|
};
|
|
|
|
@@ -287,43 +454,217 @@ void Indium::PrivateRenderCommandEncoder::setBlendColor(float red, float green,
|
|
DynamicVK::vkCmdSetBlendConstants(buf->commandBuffer(), tmp);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::updateBindings() {
|
|
- // TODO: better descriptor set resource management
|
|
-
|
|
- auto buf = _privateCommandBuffer.lock();
|
|
-
|
|
- std::array<VkDescriptorSet, 2> descriptorSets = createDescriptorSets(_privatePSO->descriptorSetLayouts().layouts, _pool, _privateDevice, { _functionResources[0], _functionResources[1] }, { _privatePSO->vertexFunctionInfo(), _privatePSO->fragmentFunctionInfo() }, _keepAliveBuffers);
|
|
+void Indium::PrivateRenderCommandEncoder::updateBindings(VkCommandBuffer commandBuffer) {
|
|
+
|
|
+ const std::array<std::reference_wrapper<const FunctionInfo>, 2> infos {
|
|
+ _privatePSO->vertexFunctionInfo(), _privatePSO->fragmentFunctionInfo()
|
|
+ };
|
|
+ for (size_t stage = 0; stage < 2; ++stage) {
|
|
+ const auto layout = _privatePSO->descriptorSetLayouts().layouts[stage];
|
|
+ const auto& current = _functionResources[stage];
|
|
+ const auto& previous = _boundResourceKeys[stage];
|
|
+ bool unchanged = _boundDescriptorSets[stage] && _boundLayouts[stage] == layout;
|
|
+ // No setter changed the stage, and the shader binding plan is identical.
|
|
+ // Checking the plan as well as the Vulkan layout matters: compatible
|
|
+ // layouts can still refer to different Metal resource indices.
|
|
+ if (unchanged && _boundFunctionInfos[stage] == &infos[stage].get() &&
|
|
+ _boundResourceRevisions[stage] == current.revision) {
|
|
+ ++_descriptorStageReuses;
|
|
+ continue;
|
|
+ }
|
|
+ // Buffers-only change (the usual draw: a new uniform offset, nothing else). Same layout, same binding plan and
|
|
+ // no texture or sampler setter since the set was bound: only the address table is rebuilt, without comparing
|
|
+ // every binding. The per-binding keys are not maintained here, so the general path below must not trust them.
|
|
+ if (unchanged && _boundFunctionInfos[stage] == &infos[stage].get() &&
|
|
+ _boundImageRevisions[stage] == current.imageRevision && _boundStageHasBuffers[stage]) {
|
|
+ const auto previousTable = _bufferAddressTables[stage];
|
|
+ auto& table = _bufferAddressTables[stage];
|
|
+ table = makeBufferAddressTable(current, infos[stage], &_resolvedResources[stage], _privateDevice, _keepAliveBuffers, _addressPool, &previousTable);
|
|
+ if (previousTable.buffer != table.buffer || previousTable.range != table.range) {
|
|
+ if (!_pool) _pool = newDescriptorPool(_privateDevice);
|
|
+ std::array<VkDescriptorSetLayout, 1> layouts {layout};
|
|
+ _boundDescriptorSets[stage] = createDescriptorSets<1>(layouts, _pool, _retiredPools, _privateDevice,
|
|
+ {std::cref(current)}, {infos[stage]}, _keepAliveBuffers, _addressPool, {&_resolvedResources[stage]}, {&table})[0];
|
|
+ }
|
|
+ _boundKeysStale[stage] = true;
|
|
+ _boundResourceRevisions[stage] = current.revision;
|
|
+ ++_descriptorStageUpdates;
|
|
+ continue;
|
|
+ }
|
|
+ bool imageBindingsUnchanged = unchanged;
|
|
+ if (_boundKeysStale[stage]) unchanged = false; // buffer keys were skipped by the fast path
|
|
+ for (const auto& binding : infos[stage].get().bindings) {
|
|
+ const auto index = binding.index;
|
|
+ if (binding.type == Iridium::BindingType::Texture)
|
|
+ imageBindingsUnchanged &= (index < current.textures.size() ? current.textures[index].get() : nullptr) ==
|
|
+ (index < previous.textures.size() ? previous.textures[index] : nullptr);
|
|
+ else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX)
|
|
+ imageBindingsUnchanged &= (index < current.samplers.size() ? current.samplers[index].get() : nullptr) ==
|
|
+ (index < previous.samplers.size() ? previous.samplers[index] : nullptr);
|
|
+ }
|
|
+
|
|
+ for (const auto& binding: infos[stage].get().bindings) {
|
|
+ if (!unchanged) break;
|
|
+ const auto index = binding.index;
|
|
+ if (binding.type == Iridium::BindingType::Buffer) {
|
|
+ const auto* a = index < current.buffers.size() ? ¤t.buffers[index] : nullptr;
|
|
+ const auto* b = index < previous.buffers.size() ? &previous.buffers[index] : nullptr;
|
|
+ unchanged = (a ? a->first.get() : nullptr) == (b ? b->first : nullptr) &&
|
|
+ (a ? a->second : 0) == (b ? b->second : 0);
|
|
+ } else if (binding.type == Iridium::BindingType::Texture) {
|
|
+ unchanged = (index < current.textures.size() ? current.textures[index].get() : nullptr) ==
|
|
+ (index < previous.textures.size() ? previous.textures[index] : nullptr);
|
|
+ } else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX) {
|
|
+ unchanged = (index < current.samplers.size() ? current.samplers[index].get() : nullptr) ==
|
|
+ (index < previous.samplers.size() ? previous.samplers[index] : nullptr);
|
|
+ }
|
|
+ }
|
|
+ if (!unchanged) {
|
|
+ const bool hasBuffers = std::any_of(infos[stage].get().bindings.begin(), infos[stage].get().bindings.end(),
|
|
+ [](const auto& binding) { return binding.type == Iridium::BindingType::Buffer; });
|
|
+ _boundStageHasBuffers[stage] = hasBuffers;
|
|
+ _boundKeysStale[stage] = false;
|
|
+ const auto previousTable = _bufferAddressTables[stage];
|
|
+ auto& table = _bufferAddressTables[stage];
|
|
+ table = hasBuffers ? makeBufferAddressTable(current, infos[stage], &_resolvedResources[stage], _privateDevice, _keepAliveBuffers, _addressPool, &previousTable) : BufferAddressTable{};
|
|
+ // Address values occupy immutable storage. Only the dynamic offset
|
|
+ // changes while images, samplers, layout, and backing chunk match.
|
|
+ if (!imageBindingsUnchanged || previousTable.buffer != table.buffer || previousTable.range != table.range) {
|
|
+ // Clear-only passes never allocate descriptors. Lease a pool
|
|
+ // only when a draw first needs a descriptor set.
|
|
+ if (!_pool) _pool = newDescriptorPool(_privateDevice);
|
|
+ std::array<VkDescriptorSetLayout, 1> layouts {layout};
|
|
+ _boundDescriptorSets[stage] = createDescriptorSets<1>(layouts, _pool, _retiredPools, _privateDevice,
|
|
+ {std::cref(current)}, {infos[stage]}, _keepAliveBuffers, _addressPool, {&_resolvedResources[stage]}, {&table})[0];
|
|
+ _boundLayouts[stage] = layout;
|
|
+ }
|
|
+ auto& keys = _boundResourceKeys[stage];
|
|
+ for (const auto& binding : infos[stage].get().bindings) {
|
|
+ const auto index = binding.index;
|
|
+ if (binding.type == Iridium::BindingType::Buffer) {
|
|
+ growToIndex(keys.buffers, index);
|
|
+ keys.buffers[index] = index < current.buffers.size() ?
|
|
+ std::make_pair(current.buffers[index].first.get(), current.buffers[index].second) : std::make_pair((Buffer*)nullptr, size_t(0));
|
|
+ } else if (binding.type == Iridium::BindingType::Texture) {
|
|
+ growToIndex(keys.textures, index);
|
|
+ keys.textures[index] = index < current.textures.size() ? current.textures[index].get() : nullptr;
|
|
+ } else if (binding.type == Iridium::BindingType::Sampler && index != SIZE_MAX) {
|
|
+ growToIndex(keys.samplers, index);
|
|
+ keys.samplers[index] = index < current.samplers.size() ? current.samplers[index].get() : nullptr;
|
|
+ }
|
|
+ }
|
|
+ // Buffer and texture resolution caches already retain these owners.
|
|
+ // Samplers are retained when their binding changes in resolveSampler().
|
|
+ ++_descriptorStageUpdates;
|
|
+ } else {
|
|
+ ++_descriptorStageReuses;
|
|
+ }
|
|
+ _boundResourceRevisions[stage] = current.revision;
|
|
+ _boundFunctionInfos[stage] = &infos[stage].get();
|
|
+ _boundImageRevisions[stage] = current.imageRevision;
|
|
+ }
|
|
+ const auto& descriptorSets = _boundDescriptorSets;
|
|
+
|
|
+ if (_depthRoot) {
|
|
+ bool feedback = false;
|
|
+ for (const auto& texture : _resolvedResources[1].textures) {
|
|
+ if (texture.depthFeedback) { feedback = true; break; }
|
|
+ }
|
|
+ if (feedback && _depthWriteRequested && !_depthWriteSuppressed) {
|
|
+ DynamicVK::vkCmdSetDepthWriteEnable(commandBuffer, VK_FALSE);
|
|
+ _depthWriteSuppressed = true;
|
|
+ static unsigned logged = 0;
|
|
+ if (logged < 5 && ++logged) {
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "depth-feedback: suppressed depth write on a draw that samples its own depth attachment\n");
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+ } else if (!feedback && _depthWriteSuppressed) {
|
|
+ DynamicVK::vkCmdSetDepthWriteEnable(commandBuffer, _depthWriteRequested ? VK_TRUE : VK_FALSE);
|
|
+ _depthWriteSuppressed = false;
|
|
+ }
|
|
+ }
|
|
|
|
- DynamicVK::vkCmdBindDescriptorSets(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, _privatePSO->pipelineLayout(), 0, descriptorSets.size(), descriptorSets.data(), 0, nullptr);
|
|
+ const auto pipelineLayout = _privatePSO->pipelineLayout();
|
|
+ std::array<uint32_t, 2> dynamicOffsets {};
|
|
+ uint32_t dynamicOffsetCount = 0;
|
|
+ for (const auto& table : _bufferAddressTables)
|
|
+ if (table.buffer) dynamicOffsets[dynamicOffsetCount++] = table.offset;
|
|
+ if (_lastDescriptorPipelineLayout != pipelineLayout || _lastDrawDescriptorSets != descriptorSets || _lastDrawDynamicOffsets != dynamicOffsets) {
|
|
+ DynamicVK::vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, descriptorSets.size(), descriptorSets.data(), dynamicOffsetCount, dynamicOffsets.data());
|
|
+ _lastDrawDynamicOffsets = dynamicOffsets;
|
|
+ _lastDescriptorPipelineLayout = pipelineLayout;
|
|
+ _lastDrawDescriptorSets = descriptorSets;
|
|
+ }
|
|
|
|
const auto& vertexInputBindings = _privatePSO->vertexInputBindings();
|
|
- if (vertexInputBindings.size() > 0) {
|
|
- std::vector<VkBuffer> buffers;
|
|
- std::vector<VkDeviceSize> offsets;
|
|
-
|
|
- buffers.resize(vertexInputBindings.size());
|
|
- offsets.resize(vertexInputBindings.size());
|
|
+ if (!vertexInputBindings.empty() &&
|
|
+ (_lastVertexResourceRevision != _functionResources[0].revision || _lastVertexBindingPSO != _privatePSO.get())) {
|
|
+ auto& buffers = _lastVertexBuffers;
|
|
+ auto& offsets = _lastVertexOffsets;
|
|
+ bool vertexBindingsChanged = buffers.size() < vertexInputBindings.size();
|
|
+ buffers.resize(vertexInputBindings.size());
|
|
+ offsets.resize(vertexInputBindings.size());
|
|
|
|
for (size_t vulkanIndex = 0; vulkanIndex < vertexInputBindings.size(); ++vulkanIndex) {
|
|
const auto& metalIndex = vertexInputBindings[vulkanIndex];
|
|
|
|
- if (metalIndex >= _functionResources[0].buffers.size()) {
|
|
- // technically, this requires the `nullDescriptor` feature, but we should never run into this case anyways.
|
|
+ PrivateBuffer* privateBuffer = nullptr;
|
|
+ size_t offset = 0;
|
|
+ if (metalIndex < _functionResources[0].buffers.size()) {
|
|
+ const auto& [buffer, bufferOffset] = _functionResources[0].buffers[metalIndex];
|
|
+ privateBuffer = metalIndex < _resolvedResources[0].buffers.size() ?
|
|
+ _resolvedResources[0].buffers[metalIndex].value : dynamic_cast<PrivateBuffer*>(buffer.get());
|
|
+ offset = bufferOffset;
|
|
+ }
|
|
+ const auto previousBuffer = buffers[vulkanIndex];
|
|
+ const auto previousOffset = offsets[vulkanIndex];
|
|
+ if (!privateBuffer) {
|
|
+ // the pipeline fetches vertices from a Metal buffer index the app never bound. Device creation enables
|
|
+ // nullDescriptor so VK_NULL_HANDLE is legal here (reads return zero); before that this faulted the GPU at address 0.
|
|
+ static unsigned unboundLogged = 0;
|
|
+ if (unboundLogged < 20) {
|
|
+ ++unboundLogged;
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "unbound-vertex-buffer: vk%zu <- mtl%u (resources.buffers.size=%zu)\n", vulkanIndex, (unsigned) metalIndex, _functionResources[0].buffers.size());
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
buffers[vulkanIndex] = VK_NULL_HANDLE;
|
|
offsets[vulkanIndex] = 0;
|
|
} else {
|
|
- auto [buffer, offset] = _functionResources[0].buffers[metalIndex];
|
|
- auto privateBuffer = std::dynamic_pointer_cast<PrivateBuffer>(buffer);
|
|
buffers[vulkanIndex] = privateBuffer->buffer();
|
|
offsets[vulkanIndex] = offset;
|
|
+ // The unique resource owner above retains this attribute buffer.
|
|
}
|
|
+ vertexBindingsChanged |= previousBuffer != buffers[vulkanIndex] || previousOffset != offsets[vulkanIndex];
|
|
}
|
|
|
|
- DynamicVK::vkCmdBindVertexBuffers(buf->commandBuffer(), 0, vertexInputBindings.size(), buffers.data(), offsets.data());
|
|
+ static unsigned bindingsLogged = 0;
|
|
+ if (bindingsLogged < 40 && Indium::spirvDebugEnabled()) {
|
|
+ ++bindingsLogged;
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "vertex-bindings (resources.buffers.size=%zu):", _functionResources[0].buffers.size());
|
|
+ for (size_t i = 0; i < vertexInputBindings.size(); ++i) {
|
|
+ fprintf(file, " [vk%zu<-mtl%u %p+%llu]", i, (unsigned) vertexInputBindings[i], (void*) buffers[i], (unsigned long long) offsets[i]);
|
|
+ }
|
|
+ fprintf(file, "\n");
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+ if (vertexBindingsChanged)
|
|
+ DynamicVK::vkCmdBindVertexBuffers(commandBuffer, 0, vertexInputBindings.size(), buffers.data(), offsets.data());
|
|
+ _lastVertexResourceRevision = _functionResources[0].revision;
|
|
+ _lastVertexBindingPSO = _privatePSO.get();
|
|
}
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount, size_t baseInstance) {
|
|
+ if (drawSkip.encoder == this && drawSkip.skip) return;
|
|
+ if (OutlineChain::shouldSkipDraw(_privatePSO.get(), vertexCount)) return;
|
|
+ beginPassIfNeeded();
|
|
+ GpuTiming::countDraw();
|
|
auto buf = _privateCommandBuffer.lock();
|
|
|
|
// bind the pipeline with the right topology class for this primitive
|
|
@@ -343,20 +684,26 @@ void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitive
|
|
default:
|
|
throw BadEnumValue();
|
|
}
|
|
- DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
-
|
|
- DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), primitiveTypeToVkPrimitiveTopology(primitiveType));
|
|
+ if (_lastDrawPipeline != pipeline) {
|
|
+ DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
+ _lastDrawPipeline = pipeline;
|
|
+ }
|
|
+ const auto topology = primitiveTypeToVkPrimitiveTopology(primitiveType);
|
|
+ if (_lastDrawTopology != topology) {
|
|
+ DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), topology);
|
|
+ _lastDrawTopology = topology;
|
|
+ }
|
|
|
|
// TODO: avoid re-binding descriptors on every draw.
|
|
- updateBindings();
|
|
+ updateBindings(buf->commandBuffer());
|
|
|
|
+ if (GpuTiming::enabled()) seqLog(" DRAW %p v=%zu i=%zu\n", (void*) _privatePSO.get(), vertexCount, instanceCount);
|
|
DynamicVK::vkCmdDraw(buf->commandBuffer(), vertexCount, instanceCount, vertexStart, baseInstance);
|
|
+ GpuTiming::afterDraw(buf->commandBuffer(), (uint64_t)(uintptr_t) _privatePSO.get());
|
|
|
|
// when we emit a draw call, we save the current function resources so that they stay alive until the command buffer is done.
|
|
// TODO: do this more efficiently by essentially doing COW: after a draw call, we only save additional references to resources
|
|
// if someone tries to overwrite them.
|
|
- _savedFunctionResources.push_back(_functionResources[0]);
|
|
- _savedFunctionResources.push_back(_functionResources[1]);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitiveType, size_t vertexStart, size_t vertexCount, size_t instanceCount) {
|
|
@@ -368,22 +715,42 @@ void Indium::PrivateRenderCommandEncoder::drawPrimitives(PrimitiveType primitive
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setVertexBytes(const void* bytes, size_t length, size_t index) {
|
|
+ if (GpuTiming::enabled()) seqLogBytes("VB", bytes, length, index);
|
|
auto cmdBuf = _privateCommandBuffer.lock();
|
|
_functionResources[0].setBytes(cmdBuf->device(), bytes, length, index);
|
|
+ resolveBuffer(0, index);
|
|
};
|
|
|
|
+void Indium::PrivateRenderCommandEncoder::beginPassIfNeeded() {
|
|
+ if (_passBegun) return;
|
|
+ _passBegun = true;
|
|
+ auto buf = _privateCommandBuffer.lock();
|
|
+ GpuTiming::beginPass(_privateDevice->device(), buf->commandBuffer(), _lazyBeginInfo.renderArea.extent.width, _lazyBeginInfo.renderArea.extent.height);
|
|
+ DynamicVK::vkCmdBeginRenderPass(buf->commandBuffer(), &_lazyBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
|
+}
|
|
+
|
|
void Indium::PrivateRenderCommandEncoder::endEncoding() {
|
|
auto buf = _privateCommandBuffer.lock();
|
|
- DynamicVK::vkCmdEndRenderPass(buf->commandBuffer());
|
|
+ if (_passBegun) {
|
|
+ DynamicVK::vkCmdEndRenderPass(buf->commandBuffer());
|
|
+ GpuTiming::endPass(buf->commandBuffer());
|
|
+ }
|
|
+ finishTrackedEncoder(buf->commandBuffer());
|
|
+ auto updates = _descriptorStageUpdates;
|
|
+ auto reuses = _descriptorStageReuses;
|
|
+ if (updates) Indium::count("descriptorStageUpdate", updates);
|
|
+ if (reuses) Indium::count("descriptorStageReuse", reuses);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setVertexBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setVertexBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) {
|
|
_functionResources[0].setBuffer(buffer, offset, index);
|
|
+ resolveBuffer(0, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setVertexBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) {
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- setVertexBuffer(buffers[i], offsets[i], range.start + i);
|
|
+ _functionResources[0].setBuffer(buffers[i], offsets[i], range.start + i);
|
|
+ resolveBuffer(0, range.start + i);
|
|
}
|
|
};
|
|
|
|
@@ -391,12 +758,14 @@ void Indium::PrivateRenderCommandEncoder::setVertexBufferOffset(size_t offset, s
|
|
_functionResources[0].setBufferOffset(offset, index);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(std::shared_ptr<SamplerState> state, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) {
|
|
_functionResources[0].setSamplerState(state, std::nullopt, index);
|
|
+ resolveSampler(0, index);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setVertexSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) {
|
|
_functionResources[0].setSamplerState(state, std::make_pair(lodMinClamp, lodMaxClamp), index);
|
|
+ resolveSampler(0, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) {
|
|
@@ -411,28 +780,34 @@ void Indium::PrivateRenderCommandEncoder::setVertexSamplerStates(const std::vect
|
|
}
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setVertexTexture(std::shared_ptr<Texture> texture, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setVertexTexture(const std::shared_ptr<Texture>& texture, size_t index) {
|
|
_functionResources[0].setTexture(texture, index);
|
|
+ resolveTexture(0, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setVertexTextures(const std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) {
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- setVertexTexture(textures[i], range.start + i);
|
|
+ _functionResources[0].setTexture(textures[i], range.start + i);
|
|
+ resolveTexture(0, range.start + i);
|
|
}
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setFragmentBytes(const void* bytes, size_t length, size_t index) {
|
|
+ if (GpuTiming::enabled()) seqLogBytes("FB", bytes, length, index);
|
|
auto cmdBuf = _privateCommandBuffer.lock();
|
|
_functionResources[1].setBytes(cmdBuf->device(), bytes, length, index);
|
|
+ resolveBuffer(1, index);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setFragmentBuffer(std::shared_ptr<Buffer> buffer, size_t offset, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setFragmentBuffer(const std::shared_ptr<Buffer>& buffer, size_t offset, size_t index) {
|
|
_functionResources[1].setBuffer(buffer, offset, index);
|
|
+ resolveBuffer(1, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setFragmentBuffers(const std::vector<std::shared_ptr<Buffer>>& buffers, const std::vector<size_t>& offsets, Range<size_t> range) {
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- setFragmentBuffer(buffers[i], offsets[i], range.start + i);
|
|
+ _functionResources[1].setBuffer(buffers[i], offsets[i], range.start + i);
|
|
+ resolveBuffer(1, range.start + i);
|
|
}
|
|
};
|
|
|
|
@@ -440,12 +815,14 @@ void Indium::PrivateRenderCommandEncoder::setFragmentBufferOffset(size_t offset,
|
|
_functionResources[1].setBufferOffset(offset, index);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(std::shared_ptr<SamplerState> state, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, size_t index) {
|
|
_functionResources[1].setSamplerState(state, std::nullopt, index);
|
|
+ resolveSampler(1, index);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(std::shared_ptr<SamplerState> state, float lodMinClamp, float lodMaxClamp, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setFragmentSamplerState(const std::shared_ptr<SamplerState>& state, float lodMinClamp, float lodMaxClamp, size_t index) {
|
|
_functionResources[1].setSamplerState(state, std::make_pair(lodMinClamp, lodMaxClamp), index);
|
|
+ resolveSampler(1, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& states, Range<size_t> range) {
|
|
@@ -460,17 +837,22 @@ void Indium::PrivateRenderCommandEncoder::setFragmentSamplerStates(const std::ve
|
|
}
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setFragmentTexture(std::shared_ptr<Texture> texture, size_t index) {
|
|
+void Indium::PrivateRenderCommandEncoder::setFragmentTexture(const std::shared_ptr<Texture>& texture, size_t index) {
|
|
_functionResources[1].setTexture(texture, index);
|
|
+ resolveTexture(1, index);
|
|
};
|
|
|
|
void Indium::PrivateRenderCommandEncoder::setFragmentTextures(std::vector<std::shared_ptr<Texture>>& textures, Range<size_t> range) {
|
|
for (size_t i = 0; i < range.length; ++i) {
|
|
- setFragmentTexture(textures[i], range.start + i);
|
|
+ _functionResources[1].setTexture(textures[i], range.start + i);
|
|
+ resolveTexture(1, range.start + i);
|
|
}
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) {
|
|
+void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount, int64_t baseVertex, size_t baseInstance) {
|
|
+ if (drawSkip.encoder == this && drawSkip.skip) return;
|
|
+ beginPassIfNeeded();
|
|
+ GpuTiming::countDraw();
|
|
auto buf = _privateCommandBuffer.lock();
|
|
|
|
// bind the pipeline with the right topology class for this primitive
|
|
@@ -490,39 +872,62 @@ void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType pr
|
|
default:
|
|
throw BadEnumValue();
|
|
}
|
|
- DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
-
|
|
- DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), primitiveTypeToVkPrimitiveTopology(primitiveType));
|
|
+ if (_lastDrawPipeline != pipeline) {
|
|
+ DynamicVK::vkCmdBindPipeline(buf->commandBuffer(), VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
+ _lastDrawPipeline = pipeline;
|
|
+ }
|
|
+ const auto topology = primitiveTypeToVkPrimitiveTopology(primitiveType);
|
|
+ if (_lastDrawTopology != topology) {
|
|
+ DynamicVK::vkCmdSetPrimitiveTopology(buf->commandBuffer(), topology);
|
|
+ _lastDrawTopology = topology;
|
|
+ }
|
|
|
|
// TODO: avoid re-binding descriptors on every draw.
|
|
- updateBindings();
|
|
-
|
|
- // we need to keep this buffer alive until we complete the render
|
|
- _keepAliveBuffers.push_back(indexBuffer);
|
|
-
|
|
- auto privateIndexBuffer = std::dynamic_pointer_cast<PrivateBuffer>(indexBuffer);
|
|
-
|
|
- DynamicVK::vkCmdBindIndexBuffer(buf->commandBuffer(), privateIndexBuffer->buffer(), indexBufferOffset, indexTypeToVkIndexType(indexType));
|
|
+ updateBindings(buf->commandBuffer());
|
|
+
|
|
+ // Earlier draws retain the owner until GPU completion; this also makes
|
|
+ // cached resource identities immune to allocator address reuse.
|
|
+ if (_lastIndexResource.object != indexBuffer.get()) {
|
|
+ auto [owner, inserted] = _resolvedBufferOwners.acquire(indexBuffer);
|
|
+ _lastIndexResource.object = indexBuffer.get();
|
|
+ _lastIndexResource.value = owner ? owner->value : nullptr;
|
|
+ }
|
|
+ const auto indexHandle = _lastIndexResource.value->buffer();
|
|
+ const auto vkIndexType = indexTypeToVkIndexType(indexType);
|
|
+ if (_lastIndexBuffer != indexHandle || _lastIndexOffset != indexBufferOffset || _lastIndexType != vkIndexType) {
|
|
+ DynamicVK::vkCmdBindIndexBuffer(buf->commandBuffer(), indexHandle, indexBufferOffset, vkIndexType);
|
|
+ _lastIndexBuffer = indexHandle;
|
|
+ _lastIndexOffset = indexBufferOffset;
|
|
+ _lastIndexType = vkIndexType;
|
|
+ }
|
|
+ if (GpuTiming::enabled()) seqLog(" DRAWI %p n=%zu i=%zu\n", (void*) _privatePSO.get(), indexCount, instanceCount);
|
|
DynamicVK::vkCmdDrawIndexed(buf->commandBuffer(), indexCount, instanceCount, 0, baseVertex, baseInstance);
|
|
+ GpuTiming::afterDraw(buf->commandBuffer(), (uint64_t)(uintptr_t) _privatePSO.get());
|
|
|
|
// see drawPrimitives() to know why we do this
|
|
- _savedFunctionResources.push_back(_functionResources[0]);
|
|
- _savedFunctionResources.push_back(_functionResources[1]);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset, size_t instanceCount) {
|
|
+void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset, size_t instanceCount) {
|
|
drawIndexedPrimitives(primitiveType, indexCount, indexType, indexBuffer, indexBufferOffset, instanceCount, 0, 0);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, std::shared_ptr<Buffer> indexBuffer, size_t indexBufferOffset) {
|
|
+void Indium::PrivateRenderCommandEncoder::drawIndexedPrimitives(PrimitiveType primitiveType, size_t indexCount, IndexType indexType, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset) {
|
|
drawIndexedPrimitives(primitiveType, indexCount, indexType, indexBuffer, indexBufferOffset, 1);
|
|
};
|
|
|
|
-void Indium::PrivateRenderCommandEncoder::setDepthStencilState(std::shared_ptr<DepthStencilState> state) {
|
|
+void Indium::PrivateRenderCommandEncoder::setDepthStencilState(const std::shared_ptr<DepthStencilState>& state) {
|
|
auto buf = _privateCommandBuffer.lock();
|
|
auto privateState = std::dynamic_pointer_cast<PrivateDepthStencilState>(state);
|
|
- auto& desc = privateState->descriptor();
|
|
-
|
|
+ if (!privateState) {
|
|
+ DynamicVK::vkCmdSetDepthTestEnable(buf->commandBuffer(), VK_FALSE);
|
|
+ DynamicVK::vkCmdSetDepthWriteEnable(buf->commandBuffer(), VK_FALSE);
|
|
+ DynamicVK::vkCmdSetStencilTestEnable(buf->commandBuffer(), VK_FALSE);
|
|
+ return;
|
|
+ }
|
|
+ auto& desc = privateState->descriptor();
|
|
+
|
|
+ _depthWriteRequested = desc.depthWriteEnabled;
|
|
+ _depthWriteSuppressed = false;
|
|
DynamicVK::vkCmdSetDepthWriteEnable(buf->commandBuffer(), desc.depthWriteEnabled ? VK_TRUE : VK_FALSE);
|
|
DynamicVK::vkCmdSetDepthCompareOp(buf->commandBuffer(), compareFunctionToVkCompareOp(desc.depthCompareFunction));
|
|
DynamicVK::vkCmdSetDepthTestEnable(buf->commandBuffer(), VK_TRUE);
|
|
@@ -694,3 +1099,18 @@ void Indium::PrivateRenderCommandEncoder::useResources(const std::vector<std::sh
|
|
// TODO: check what Metal does in this case. this is just an educated guess
|
|
useResources(resources, usage, RenderStages::Vertex | RenderStages::Fragment | RenderStages::Tile | RenderStages::Object | RenderStages::Mesh);
|
|
};
|
|
+
|
|
+void Indium::PrivateRenderCommandEncoder::memoryBarrier(size_t scope, RenderStages after, RenderStages before) {
|
|
+ if (after != RenderStages::Fragment || before != RenderStages::Fragment)
|
|
+ throw std::invalid_argument("Only fragment-to-fragment render barriers are supported");
|
|
+ if (!scope || (scope & ~size_t(7))) throw std::invalid_argument("Invalid render barrier scope");
|
|
+ auto buf = _privateCommandBuffer.lock();
|
|
+ VkMemoryBarrier barrier {};
|
|
+ barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER;
|
|
+ if (scope & 3) barrier.srcAccessMask |= VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
|
|
+ if (scope & 4) barrier.srcAccessMask |= VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
+ barrier.dstAccessMask = barrier.srcAccessMask;
|
|
+ VkPipelineStageFlags stages = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
|
|
+ if (scope & 4) stages |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
+ DynamicVK::vkCmdPipelineBarrier(buf->commandBuffer(), stages, stages, VK_DEPENDENCY_BY_REGION_BIT, 1, &barrier, 0, nullptr, 0, nullptr);
|
|
+}
|
|
diff --git a/src/indium/render-pipeline.cpp b/src/indium/render-pipeline.cpp
|
|
index 1b6bb3b..97a1116 100644
|
|
--- a/src/indium/render-pipeline.cpp
|
|
+++ b/src/indium/render-pipeline.cpp
|
|
@@ -1,8 +1,13 @@
|
|
+#include <indium/outline-chain.private.hpp>
|
|
+#include <indium/gpu-timing.private.hpp>
|
|
+#include <unistd.h>
|
|
+#include <cstdio>
|
|
#include <indium/render-pipeline.private.hpp>
|
|
#include <indium/device.private.hpp>
|
|
#include <indium/library.private.hpp>
|
|
#include <indium/types.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/counters.private.hpp>
|
|
|
|
#include <stdexcept>
|
|
|
|
@@ -22,9 +27,33 @@ Indium::PrivateRenderPipelineState::PrivateRenderPipelineState(std::shared_ptr<P
|
|
|
|
_vertexFunction = std::dynamic_pointer_cast<PrivateFunction>(descriptor.vertexFunction);
|
|
_fragmentFunction = std::dynamic_pointer_cast<PrivateFunction>(descriptor.fragmentFunction);
|
|
-
|
|
- _descriptorSetLayouts.processFunction(_vertexFunction, 0);
|
|
- _descriptorSetLayouts.processFunction(_fragmentFunction, 1);
|
|
+ GpuTiming::notePipeline(this, _vertexFunction ? GpuTiming::libraryId(_vertexFunction->library().get()) : std::string("-"), _fragmentFunction ? GpuTiming::libraryId(_fragmentFunction->library().get()) : std::string("-"));
|
|
+ OutlineChain::notePipeline(this, _fragmentFunction ? (const void*) _fragmentFunction->library().get() : nullptr);
|
|
+
|
|
+ if (Indium::spirvDebugEnabled()) {
|
|
+ static unsigned created = 0;
|
|
+ if (created++ < 8192) {
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "indium-pso %p vs-lib=%p fs-lib=%p layouts:", (void*) this,
|
|
+ _vertexFunction ? (void*) _vertexFunction->library().get() : nullptr, _fragmentFunction ? (void*) _fragmentFunction->library().get() : nullptr);
|
|
+ if (_vertexDescriptor) {
|
|
+ for (const auto& [index, layout]: _vertexDescriptor->layouts) {
|
|
+ fprintf(file, " [%zu stride=%zu step=%d]", (size_t) index, (size_t) layout.stride, (int) layout.stepFunction);
|
|
+ }
|
|
+ fprintf(file, " attrs:");
|
|
+ for (const auto& [index, attr]: _vertexDescriptor->attributes) {
|
|
+ fprintf(file, " [%zu fmt=%d off=%zu buf=%zu]", (size_t) index, (int) attr.format, (size_t) attr.offset, (size_t) attr.bufferIndex);
|
|
+ }
|
|
+ } else {
|
|
+ fprintf(file, " none");
|
|
+ }
|
|
+ fprintf(file, "\n");
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ _descriptorSetLayouts.processFunction(_vertexFunction, 0, true);
|
|
+ _descriptorSetLayouts.processFunction(_fragmentFunction, 1, true);
|
|
};
|
|
|
|
Indium::PrivateRenderPipelineState::~PrivateRenderPipelineState() {
|
|
@@ -67,6 +96,14 @@ void Indium::PrivateRenderPipelineState::recreatePipeline(VkRenderPass compatibl
|
|
shaderStage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
|
|
shaderStage.stage = VK_SHADER_STAGE_VERTEX_BIT;
|
|
+ static unsigned libsLogged = 0;
|
|
+ if (libsLogged < 8192 && Indium::spirvDebugEnabled()) {
|
|
+ ++libsLogged;
|
|
+ if (FILE* file = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(file, "pipeline-libs vs=%p fs=%p\n", (void*) _vertexFunction->library().get(), _fragmentFunction ? (void*) _fragmentFunction->library().get() : nullptr);
|
|
+ fclose(file);
|
|
+ }
|
|
+ }
|
|
shaderStage.module = _vertexFunction->library()->shaderModule();
|
|
shaderStage.pName = vertexFunctionName.c_str();
|
|
stages.push_back(shaderStage);
|
|
@@ -168,6 +205,7 @@ void Indium::PrivateRenderPipelineState::recreatePipeline(VkRenderPass compatibl
|
|
tmp.dstAlphaBlendFactor = blendFactorToVkBlendFactor(colorInfo.destinationAlphaBlendFactor);
|
|
tmp.alphaBlendOp = blendOperationToVkBlendOp(colorInfo.alphaBlendOperation);
|
|
tmp.colorWriteMask = colorWriteMaskToVkColorComponentFlags(colorInfo.writeMask);
|
|
+ if (colorInfo.pixelFormat == PixelFormat::Invalid) tmp.colorWriteMask = 0;
|
|
colorBlendStates.push_back(tmp);
|
|
}
|
|
VkPipelineColorBlendStateCreateInfo colorBlendState {};
|
|
diff --git a/src/indium/sampler.cpp b/src/indium/sampler.cpp
|
|
index 71392df..d82e5f0 100644
|
|
--- a/src/indium/sampler.cpp
|
|
+++ b/src/indium/sampler.cpp
|
|
@@ -19,10 +19,14 @@ Indium::PrivateSamplerState::PrivateSamplerState(std::shared_ptr<PrivateDevice>
|
|
info.mipLodBias = 0; // not sure where to get this from
|
|
info.anisotropyEnable = (descriptor.maxAnisotropy > 1) ? VK_TRUE : VK_FALSE;
|
|
info.maxAnisotropy = descriptor.maxAnisotropy;
|
|
- info.compareEnable = descriptor.normalizedCoordinates ? VK_TRUE : VK_FALSE; // FIXME: this assumes that the implementation defers to whatever the shader state specifies
|
|
+ // Metal only uses the compare function for comparison sampling (shadow maps); a sampler left at Never is used for
|
|
+ // ordinary reads, which are invalid (and in practice return nothing useful) with a Vulkan comparison sampler.
|
|
+ info.compareEnable = (descriptor.normalizedCoordinates && descriptor.compareFunction != CompareFunction::Never) ? VK_TRUE : VK_FALSE;
|
|
info.compareOp = compareFunctionToVkCompareOp(descriptor.compareFunction);
|
|
- info.minLod = descriptor.normalizedCoordinates ? descriptor.lodMinClamp : 0;
|
|
- info.maxLod = descriptor.normalizedCoordinates ? descriptor.lodMaxClamp : 0;
|
|
+ // A sampler without a mip filter reads the base level only, whatever the texture has.
|
|
+ const bool mipmapped = descriptor.mipFilter != SamplerMipFilter::NotMipmapped;
|
|
+ info.minLod = (descriptor.normalizedCoordinates && mipmapped) ? descriptor.lodMinClamp : 0;
|
|
+ info.maxLod = (descriptor.normalizedCoordinates && mipmapped) ? descriptor.lodMaxClamp : 0;
|
|
info.borderColor = samplerBorderColorToVkBorderColor(descriptor.borderColor);
|
|
info.unnormalizedCoordinates = descriptor.normalizedCoordinates ? VK_FALSE : VK_TRUE;
|
|
|
|
diff --git a/src/indium/texture.cpp b/src/indium/texture.cpp
|
|
index 1e3e971..cbdd7ff 100644
|
|
--- a/src/indium/texture.cpp
|
|
+++ b/src/indium/texture.cpp
|
|
@@ -1,8 +1,90 @@
|
|
+#include <indium/submit-batch.private.hpp>
|
|
+#include <indium/submit-thread.private.hpp>
|
|
+#include <indium/deferred-render-encoder.private.hpp>
|
|
+#include <unistd.h>
|
|
#include <indium/texture.private.hpp>
|
|
+#include <indium/memory-pool.private.hpp>
|
|
#include <indium/device.private.hpp>
|
|
#include <indium/types.private.hpp>
|
|
#include <indium/buffer.private.hpp>
|
|
#include <indium/dynamic-vk.hpp>
|
|
+#include <indium/counters.private.hpp>
|
|
+#include <chrono>
|
|
+#include <atomic>
|
|
+#include <deque>
|
|
+#include <memory>
|
|
+
|
|
+// Texture creation (layout transition) and replaceRegion (staging copy) each submit a one-shot command buffer. They used to
|
|
+// wait for its fence, and that fence only signals once the GPU has also finished everything submitted before it: in a League
|
|
+// fight the main thread spent 5.7% of its time in that wait (DRM_IOCTL_SYNCOBJ_WAIT, sampled 2026-10-10). The work is ordered
|
|
+// by the queue, so later command buffers see its result without anyone waiting; only the one-shot buffer, its fence and the
|
|
+// staging buffer have to outlive it. They are parked here and released once their fence has signalled.
|
|
+// MEASURED SLOWER and therefore OFF by default: three interleaved fight pairs gave 81.6/78.1/78.4 fps without the wait
|
|
+// against 84.9/81.9/81.7 with it (2026-10-10, GPU 95-97% busy, so the wait was not costing throughput).
|
|
+// Guest file /tmp/metal-async-uploads enables it (re-read every 64 one-shot submissions).
|
|
+namespace {
|
|
+ struct PendingOneshot {
|
|
+ VkFence fence;
|
|
+ VkCommandBuffer commandBuffer;
|
|
+ VkImage image;
|
|
+ std::shared_ptr<void> keepAlive;
|
|
+ };
|
|
+
|
|
+ // guarded by the device's oneshotCommandPoolMutex
|
|
+ std::deque<PendingOneshot>& pendingOneshots() {
|
|
+ static auto* list = new std::deque<PendingOneshot>;
|
|
+ return *list;
|
|
+ }
|
|
+
|
|
+ bool syncUploads() {
|
|
+ static std::atomic<bool> sync {true};
|
|
+ static std::atomic<unsigned> calls {0};
|
|
+ if ((calls.fetch_add(1, std::memory_order_relaxed) & 63) == 0)
|
|
+ sync.store(access("/tmp/metal-async-uploads", F_OK) != 0, std::memory_order_relaxed);
|
|
+ return sync.load(std::memory_order_relaxed);
|
|
+ }
|
|
+
|
|
+ void releaseOneshot(const std::shared_ptr<Indium::PrivateDevice>& device, PendingOneshot& entry) {
|
|
+ Indium::DynamicVK::vkDestroyFence(device->device(), entry.fence, nullptr);
|
|
+ Indium::DynamicVK::vkFreeCommandBuffers(device->device(), device->oneshotCommandPool(), 1, &entry.commandBuffer);
|
|
+ }
|
|
+
|
|
+ // caller holds the pool mutex. Releases finished entries in submission order; with `through` set, waits for and releases
|
|
+ // everything up to the last entry that uses that image (the image is about to be destroyed).
|
|
+ void retireOneshots(const std::shared_ptr<Indium::PrivateDevice>& device, VkImage through = VK_NULL_HANDLE) {
|
|
+ auto& list = pendingOneshots();
|
|
+ size_t mustWait = 0;
|
|
+ if (through != VK_NULL_HANDLE) {
|
|
+ for (size_t i = 0; i < list.size(); ++i) if (list[i].image == through) mustWait = i + 1;
|
|
+ }
|
|
+ if (list.size() > 512 && mustWait < list.size() - 512) mustWait = list.size() - 512; // bounded backlog
|
|
+ while (!list.empty()) {
|
|
+ auto& front = list.front();
|
|
+ if (mustWait > 0) {
|
|
+ Indium::Waits::Block block("oneshotFence");
|
|
+ if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &front.fence, VK_TRUE, 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) abort();
|
|
+ --mustWait;
|
|
+ } else if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &front.fence, VK_TRUE, 0) != VK_SUCCESS) { // zero timeout: a status poll
|
|
+ break;
|
|
+ }
|
|
+ releaseOneshot(device, front);
|
|
+ list.pop_front();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // caller holds the pool mutex and has submitted `commandBuffer` with `fence`
|
|
+ void finishOneshot(const std::shared_ptr<Indium::PrivateDevice>& device, VkFence fence, VkCommandBuffer commandBuffer, VkImage image, std::shared_ptr<void> keepAlive) {
|
|
+ PendingOneshot entry {fence, commandBuffer, image, std::move(keepAlive)};
|
|
+ if (syncUploads()) {
|
|
+ // this is a GPU wait on the uploading thread; counted under /tmp/metal-debug-waits like the hand-off waits
|
|
+ Indium::Waits::Block block("oneshotFence");
|
|
+ if (Indium::DynamicVK::vkWaitForFences(device->device(), 1, &fence, VK_TRUE, 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) abort();
|
|
+ releaseOneshot(device, entry);
|
|
+ return;
|
|
+ }
|
|
+ pendingOneshots().push_back(std::move(entry));
|
|
+ }
|
|
+}
|
|
|
|
#include <cstring>
|
|
|
|
@@ -37,8 +119,8 @@ std::shared_ptr<Indium::Texture> Indium::PrivateTexture::newTextureView(PixelFor
|
|
return newTextureView(
|
|
pixelFormat,
|
|
textureType(),
|
|
- Range<size_t> { parentRelativeLevel(), mipmapLevelCount() },
|
|
- Range<size_t> { parentRelativeSlice(), arrayLength() }
|
|
+ Range<size_t> { 0, mipmapLevelCount() },
|
|
+ Range<size_t> { 0, arrayLength() }
|
|
);
|
|
};
|
|
|
|
@@ -78,12 +160,16 @@ Indium::TextureView::TextureView(std::shared_ptr<PrivateTexture> original, Pixel
|
|
_layers(layers),
|
|
_swizzle(swizzle)
|
|
{
|
|
- auto parentLevelStart = _original->parentRelativeLevel();
|
|
+ size_t parentLevelStart = 0;
|
|
+ size_t parentLayerStart = 0;
|
|
+ for (std::shared_ptr<Texture> parent = _original; parent; parent = parent->parentTexture()) {
|
|
+ parentLevelStart += parent->parentRelativeLevel();
|
|
+ parentLayerStart += parent->parentRelativeSlice();
|
|
+ }
|
|
auto parentLevelEnd = parentLevelStart + _original->mipmapLevelCount() - 1;
|
|
auto levelStart = std::min(parentLevelStart + _levels.start, parentLevelEnd);
|
|
auto levelEnd = std::min(levelStart + _levels.length - 1, parentLevelEnd);
|
|
|
|
- auto parentLayerStart = _original->parentRelativeSlice();
|
|
auto parentLayerEnd = parentLayerStart + _original->arrayLength() - 1;
|
|
auto layerStart = std::min(parentLayerStart + _layers.start, parentLayerEnd);
|
|
auto layerEnd = std::min(layerStart + _layers.length - 1, parentLayerEnd);
|
|
@@ -109,9 +195,16 @@ Indium::TextureView::TextureView(std::shared_ptr<PrivateTexture> original, Pixel
|
|
// TODO
|
|
abort();
|
|
}
|
|
+ if ((info.subresourceRange.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
|
+ // Combined depth/stencil is legal for attachments, but descriptors require one aspect.
|
|
+ info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
+ if (DynamicVK::vkCreateImageView(_device->device(), &info, nullptr, &_sampledImageView) != VK_SUCCESS) abort();
|
|
+ }
|
|
+
|
|
};
|
|
|
|
Indium::TextureView::~TextureView() {
|
|
+ if (_sampledImageView) DynamicVK::vkDestroyImageView(_device->device(), _sampledImageView, nullptr);
|
|
DynamicVK::vkDestroyImageView(std::dynamic_pointer_cast<PrivateDevice>(_original->device())->device(), _imageView, nullptr);
|
|
};
|
|
|
|
@@ -124,15 +217,15 @@ Indium::TextureType Indium::TextureView::textureType() const {
|
|
};
|
|
|
|
size_t Indium::TextureView::width() const {
|
|
- return _original->width();
|
|
+ return std::max(size_t(1), _original->width() >> _levels.start);
|
|
};
|
|
|
|
size_t Indium::TextureView::height() const {
|
|
- return _original->height();
|
|
+ return std::max(size_t(1), _original->height() >> _levels.start);
|
|
};
|
|
|
|
size_t Indium::TextureView::depth() const {
|
|
- return _original->depth();
|
|
+ return std::max(size_t(1), _original->depth() >> _levels.start);
|
|
};
|
|
|
|
size_t Indium::TextureView::mipmapLevelCount() const {
|
|
@@ -326,13 +419,42 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr<PrivateDevice> device,
|
|
|
|
info.tiling = (descriptor.allowGPUOptimizedContents || !canBeLinear) ? VK_IMAGE_TILING_OPTIMAL : VK_IMAGE_TILING_LINEAR;
|
|
|
|
- // we don't know ahead of time how the image is going to be used, so specify everything we support
|
|
- info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
- if (isColor) {
|
|
+ // Optional usages must be supported by the format and selected tiling.
|
|
+ // Compressed/sRGB formats commonly cannot be storage images or attachments.
|
|
+ VkFormatProperties formatProperties {};
|
|
+ DynamicVK::vkGetPhysicalDeviceFormatProperties(_device->physicalDevice(), info.format, &formatProperties);
|
|
+ if (info.tiling == VK_IMAGE_TILING_LINEAR && !formatProperties.linearTilingFeatures) {
|
|
+ info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
+ }
|
|
+ VkFormatFeatureFlags formatFeatures = info.tiling == VK_IMAGE_TILING_LINEAR ? formatProperties.linearTilingFeatures : formatProperties.optimalTilingFeatures;
|
|
+ info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
+ // RADV cannot keep DCC (colour compression) on GFX9 for an image that may be a storage image, so every render target
|
|
+ // that carried STORAGE_BIT cost full uncompressed bandwidth. A render target that Metal does not declare
|
|
+ // ShaderWrite does not need it. Guest flag file /tmp/metal-keep-storage restores the old behaviour for A/B runs.
|
|
+ static const bool keepStorage = access("/tmp/metal-keep-storage", F_OK) == 0;
|
|
+ const size_t metalUsage = static_cast<size_t>(_descriptor.usage);
|
|
+ const bool renderTargetOnly = (metalUsage & static_cast<size_t>(TextureUsage::RenderTarget)) != 0 &&
|
|
+ (metalUsage & static_cast<size_t>(TextureUsage::ShaderWrite)) == 0;
|
|
+ if ((formatFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) && (keepStorage || !renderTargetOnly)) {
|
|
+ info.usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
|
+ }
|
|
+ if (isColor && (formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
|
|
info.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
|
- } else if (isDepthStencil) {
|
|
+ } else if (isDepthStencil && (formatFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
|
|
info.usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
|
}
|
|
+ {
|
|
+ static const bool logTextures = access("/tmp/metal-gpu-timing", F_OK) == 0;
|
|
+ if (logTextures) {
|
|
+ if (FILE* f = fopen("/tmp/indium-textures.log", "a")) {
|
|
+ fprintf(f, "%ux%ux%u mips=%u fmt=%u usage=%zu opt=%d tiling=%s storage=%d storageMode=%u\n", (unsigned) _descriptor.width,
|
|
+ (unsigned) _descriptor.height, (unsigned) _descriptor.depth, (unsigned) _descriptor.mipmapLevelCount, (unsigned) _descriptor.pixelFormat,
|
|
+ metalUsage, (int) descriptor.allowGPUOptimizedContents, info.tiling == VK_IMAGE_TILING_LINEAR ? "LINEAR" : "OPTIMAL",
|
|
+ (info.usage & VK_IMAGE_USAGE_STORAGE_BIT) ? 1 : 0, (unsigned) _storageMode);
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; // TODO: check if this should be "concurrent" instead. i think we're good, though.
|
|
info.queueFamilyIndexCount = 0;
|
|
info.pQueueFamilyIndices = nullptr;
|
|
@@ -347,45 +469,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr<PrivateDevice> device,
|
|
|
|
DynamicVK::vkGetImageMemoryRequirements(_device->device(), _image, &reqs);
|
|
|
|
- size_t targetIndex = SIZE_MAX;
|
|
-
|
|
- for (size_t i = 0; i < _device->memoryProperties().memoryTypeCount; ++i) {
|
|
- const auto& type = _device->memoryProperties().memoryTypes[i];
|
|
-
|
|
- if ((reqs.memoryTypeBits & (1 << i)) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- if ((_storageMode == StorageMode::Managed || _storageMode == StorageMode::Shared) && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- if (_storageMode == StorageMode::Shared && (type.propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0) {
|
|
- continue;
|
|
- }
|
|
-
|
|
- // okay, this is good enough
|
|
- targetIndex = i;
|
|
- break;
|
|
- }
|
|
+ size_t targetIndex = _device->findMemoryType(reqs.memoryTypeBits, _storageMode, true);
|
|
|
|
if (targetIndex == SIZE_MAX) {
|
|
throw std::runtime_error("No suitable memory region found for image with requested storage mode");
|
|
}
|
|
|
|
- VkMemoryAllocateInfo allocateInfo {};
|
|
- allocateInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
- allocateInfo.allocationSize = reqs.size;
|
|
- allocateInfo.memoryTypeIndex = targetIndex;
|
|
+ Indium::count("textureAlloc", reqs.size);
|
|
+ auto allocation = MemoryPool::allocate(_device->device(), reinterpret_cast<uint64_t>(_image), targetIndex, reqs.size, reqs.alignment, true, false);
|
|
+ _memory = allocation.memory;
|
|
|
|
- if (DynamicVK::vkAllocateMemory(_device->device(), &allocateInfo, nullptr, &_memory) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
-
|
|
- DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, 0);
|
|
+ DynamicVK::vkBindImageMemory(_device->device(), _image, _memory, allocation.offset);
|
|
|
|
// transition the image into the general layout
|
|
+ // Drain before taking the upload pool lock: a queued commit may need the pool to construct its batch barrier.
|
|
+ SubmitThread::drain();
|
|
+ flushPendingSubmits();
|
|
+ std::unique_lock<std::mutex> poolLock(_device->oneshotCommandPoolMutex());
|
|
+ retireOneshots(_device);
|
|
VkCommandBufferAllocateInfo cmdBufAllocInfo {};
|
|
cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
@@ -409,7 +510,7 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr<PrivateDevice> device,
|
|
barrier.srcAccessMask = VK_ACCESS_NONE;
|
|
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
- barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
+ barrier.newLayout = imageLayout();
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.image = _image;
|
|
@@ -438,15 +539,13 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr<PrivateDevice> device,
|
|
submitInfo.commandBufferCount = 1;
|
|
submitInfo.pCommandBuffers = &cmdBuf;
|
|
|
|
- DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence);
|
|
- if (DynamicVK::vkWaitForFences(_device->device(), 1, &theFence, VK_TRUE, /* 1s */ 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
+ {
|
|
+ flushPendingSubmits(); // same for commits merged into a pending batch (takes the queue mutex itself)
|
|
+ std::lock_guard<std::mutex> queueLock(_device->graphicsQueueMutex());
|
|
+ if (DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence) != VK_SUCCESS) abort();
|
|
}
|
|
-
|
|
- DynamicVK::vkDestroyFence(_device->device(), theFence, nullptr);
|
|
-
|
|
- DynamicVK::vkFreeCommandBuffers(_device->device(), _device->oneshotCommandPool(), 1, &cmdBuf);
|
|
+ finishOneshot(_device, theFence, cmdBuf, _image, nullptr);
|
|
+ poolLock.unlock();
|
|
|
|
VkImageViewCreateInfo info2 {};
|
|
info2.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
@@ -463,12 +562,24 @@ Indium::ConcreteTexture::ConcreteTexture(std::shared_ptr<PrivateDevice> device,
|
|
// TODO
|
|
abort();
|
|
}
|
|
+ if ((info2.subresourceRange.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) == (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
|
+ // Combined depth/stencil is legal for attachments, but descriptors require one aspect.
|
|
+ info2.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
+ if (DynamicVK::vkCreateImageView(_device->device(), &info2, nullptr, &_sampledImageView) != VK_SUCCESS) abort();
|
|
+ }
|
|
+
|
|
};
|
|
|
|
Indium::ConcreteTexture::~ConcreteTexture() {
|
|
+ {
|
|
+ std::unique_lock<std::mutex> poolLock(_device->oneshotCommandPoolMutex());
|
|
+ retireOneshots(_device, _image);
|
|
+ }
|
|
+ if (_sampledImageView) DynamicVK::vkDestroyImageView(_device->device(), _sampledImageView, nullptr);
|
|
DynamicVK::vkDestroyImageView(_device->device(), _imageView, nullptr);
|
|
+ auto owner = reinterpret_cast<uint64_t>(_image);
|
|
DynamicVK::vkDestroyImage(_device->device(), _image, nullptr);
|
|
- DynamicVK::vkFreeMemory(_device->device(), _memory, nullptr);
|
|
+ MemoryPool::free(_device->device(), owner);
|
|
};
|
|
|
|
Indium::TextureType Indium::ConcreteTexture::textureType() const {
|
|
@@ -537,7 +648,20 @@ VkImage Indium::ConcreteTexture::image() {
|
|
return _image;
|
|
};
|
|
|
|
+// RADV (GFX9) keeps colour compression (DCC) only outside VK_IMAGE_LAYOUT_GENERAL. A plain colour render target that is
|
|
+// also sampled (Metal usage RenderTarget|ShaderRead) rests in SHADER_READ_ONLY_OPTIMAL and is a COLOR_ATTACHMENT_OPTIMAL
|
|
+// attachment; everything else keeps GENERAL. Guest flag file /tmp/metal-no-opt-layouts disables it (checked once).
|
|
VkImageLayout Indium::ConcreteTexture::imageLayout() {
|
|
+ static const bool optLayouts = access("/tmp/metal-no-opt-layouts", F_OK) != 0;
|
|
+ if (optLayouts) {
|
|
+ const size_t usage = static_cast<size_t>(_descriptor.usage);
|
|
+ const size_t renderTarget = static_cast<size_t>(TextureUsage::RenderTarget);
|
|
+ const size_t shaderWrite = static_cast<size_t>(TextureUsage::ShaderWrite);
|
|
+ if ((usage & renderTarget) && !(usage & shaderWrite) && _descriptor.textureType == TextureType::e2D &&
|
|
+ _descriptor.mipmapLevelCount == 1 && _descriptor.arrayLength == 1 &&
|
|
+ (pixelFormatToVkImageAspectFlags(_descriptor.pixelFormat) & VK_IMAGE_ASPECT_COLOR_BIT))
|
|
+ return VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
+ }
|
|
return VK_IMAGE_LAYOUT_GENERAL;
|
|
};
|
|
|
|
@@ -546,6 +670,16 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap
|
|
};
|
|
|
|
void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmapLevel, size_t slice, const void* bytes, size_t bytesPerRow, size_t bytesPerImage) {
|
|
+ // diagnostic counters (only active with the guest /tmp/metal-debug-timing flag): how often CPU uploads happen and how long the
|
|
+ // synchronous staging copy + fence wait blocks the calling thread
|
|
+ const auto uploadStart = std::chrono::steady_clock::now();
|
|
+ struct UploadTimer {
|
|
+ std::chrono::steady_clock::time_point start; size_t bytes;
|
|
+ ~UploadTimer() {
|
|
+ Indium::count("replaceRegionCalls", bytes);
|
|
+ Indium::count("replaceRegionMicroseconds", std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - start).count());
|
|
+ }
|
|
+ } uploadTimer {uploadStart, bytesPerImage ? bytesPerImage : bytesPerRow * region.size.height};
|
|
if (_storageMode != StorageMode::Managed && _storageMode != StorageMode::Shared) {
|
|
throw std::runtime_error("Invalid usage of replaceRegion on non-managed and non-shared texture");
|
|
}
|
|
@@ -556,99 +690,109 @@ void Indium::ConcreteTexture::replaceRegion(Indium::Region region, size_t mipmap
|
|
|
|
auto aspect = pixelFormatToVkImageAspectFlags(_descriptor.pixelFormat);
|
|
|
|
- // create a temporary buffer copy
|
|
- // FIXME: this doesn't work for PVRTC textures
|
|
- // actually, FIXME: this doesn't handle a few cases, particularly with array texture types (since i haven't seen those yet at all)
|
|
- auto byteSize = bytesPerImage == 0 ? (region.size.height * bytesPerRow) : (region.size.depth * bytesPerImage);
|
|
- auto tmpBuf = std::dynamic_pointer_cast<PrivateBuffer>(_device->newBuffer(bytes, byteSize, ResourceOptions::StorageModeShared));
|
|
-
|
|
- // FIXME: handle compressed formats
|
|
- size_t bytesPerPixel = pixelFormatToByteCount(_descriptor.pixelFormat);
|
|
-
|
|
- VkCommandBufferAllocateInfo cmdBufAllocInfo {};
|
|
- cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
- cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
- cmdBufAllocInfo.commandPool = _device->oneshotCommandPool();
|
|
- cmdBufAllocInfo.commandBufferCount = 1;
|
|
-
|
|
- VkCommandBuffer cmdBuf;
|
|
- if (DynamicVK::vkAllocateCommandBuffers(_device->device(), &cmdBufAllocInfo, &cmdBuf) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
-
|
|
- VkCommandBufferBeginInfo cmdBufBeginInfo {};
|
|
- cmdBufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
- cmdBufBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
-
|
|
- DynamicVK::vkBeginCommandBuffer(cmdBuf, &cmdBufBeginInfo);
|
|
-
|
|
- // first, transition the image to the optimal layout for transfer destinations
|
|
- VkImageMemoryBarrier barrier {};
|
|
- barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
- barrier.srcAccessMask = VK_ACCESS_NONE;
|
|
- barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
- barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
- barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
- barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
- barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
- barrier.image = _image;
|
|
- barrier.subresourceRange.aspectMask = aspect;
|
|
- barrier.subresourceRange.baseMipLevel = mipmapLevel;
|
|
- barrier.subresourceRange.levelCount = 1;
|
|
- barrier.subresourceRange.baseArrayLayer = slice;
|
|
- barrier.subresourceRange.layerCount = 1;
|
|
-
|
|
- DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_NONE, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
-
|
|
- // now encode the copy
|
|
- VkBufferImageCopy copyInfo {};
|
|
- copyInfo.bufferOffset = 0;
|
|
- copyInfo.bufferRowLength = bytesPerRow / bytesPerPixel;
|
|
- copyInfo.bufferImageHeight = bytesPerImage / bytesPerRow;
|
|
- copyInfo.imageSubresource.aspectMask = aspect;
|
|
- copyInfo.imageSubresource.mipLevel = mipmapLevel;
|
|
- copyInfo.imageSubresource.baseArrayLayer = slice;
|
|
- copyInfo.imageSubresource.layerCount = 1;
|
|
- copyInfo.imageOffset.x = region.origin.x;
|
|
- copyInfo.imageOffset.y = region.origin.y;
|
|
- copyInfo.imageOffset.z = region.origin.z;
|
|
- copyInfo.imageExtent.width = region.size.width;
|
|
- copyInfo.imageExtent.height = region.size.height;
|
|
- copyInfo.imageExtent.depth = region.size.depth;
|
|
- DynamicVK::vkCmdCopyBufferToImage(cmdBuf, tmpBuf->buffer(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info);
|
|
-
|
|
- // finally, transition the image back to the general layout
|
|
- barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
- barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
- barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
- barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
- DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
-
|
|
- DynamicVK::vkEndCommandBuffer(cmdBuf);
|
|
-
|
|
- VkFenceCreateInfo fenceCreateInfo {};
|
|
- fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
|
-
|
|
- VkFence theFence = VK_NULL_HANDLE;
|
|
+ auto pitch = bufferImagePitch(_descriptor.pixelFormat, region.size.width, region.size.height, bytesPerRow, bytesPerImage);
|
|
+ auto block = pixelFormatTransferBlock(_descriptor.pixelFormat);
|
|
+ size_t rows = (region.size.height + block.height - 1) / block.height;
|
|
+ size_t tightRow = ((region.size.width + block.width - 1) / block.width) * block.bytes;
|
|
+ size_t byteSize = (region.size.depth - 1) * pitch.imageBytes + (rows - 1) * pitch.rowBytes + tightRow;
|
|
+ auto tmpBuf = std::dynamic_pointer_cast<PrivateBuffer>(_device->newBuffer(bytes, byteSize, ResourceOptions::StorageModeShared));
|
|
+
|
|
+ // With deferred encoding on, the upload is queued behind the pending replays on the same worker (FIFO keeps the order)
|
|
+ // instead of draining it from this thread in the middle of a frame.
|
|
+ auto upload = [=, self = shared_from_this()]() {
|
|
+ // Drain before taking the upload pool lock: a queued commit may need the pool to construct its batch barrier.
|
|
+ SubmitThread::drain();
|
|
+ flushPendingSubmits();
|
|
+ std::unique_lock<std::mutex> poolLock(_device->oneshotCommandPoolMutex());
|
|
+ retireOneshots(_device);
|
|
+ VkCommandBufferAllocateInfo cmdBufAllocInfo {};
|
|
+ cmdBufAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
|
|
+ cmdBufAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
+ cmdBufAllocInfo.commandPool = _device->oneshotCommandPool();
|
|
+ cmdBufAllocInfo.commandBufferCount = 1;
|
|
+
|
|
+ VkCommandBuffer cmdBuf;
|
|
+ if (DynamicVK::vkAllocateCommandBuffers(_device->device(), &cmdBufAllocInfo, &cmdBuf) != VK_SUCCESS) {
|
|
+ // TODO
|
|
+ abort();
|
|
+ }
|
|
|
|
- if (DynamicVK::vkCreateFence(_device->device(), &fenceCreateInfo, nullptr, &theFence) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
- }
|
|
+ VkCommandBufferBeginInfo cmdBufBeginInfo {};
|
|
+ cmdBufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
|
|
+ cmdBufBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
+
|
|
+ DynamicVK::vkBeginCommandBuffer(cmdBuf, &cmdBufBeginInfo);
|
|
+
|
|
+ // first, transition the image to the optimal layout for transfer destinations
|
|
+ VkImageMemoryBarrier barrier {};
|
|
+ barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
+ barrier.srcAccessMask = VK_ACCESS_NONE;
|
|
+ barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
+ barrier.oldLayout = imageLayout();
|
|
+ barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
+ barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
+ barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
+ barrier.image = _image;
|
|
+ barrier.subresourceRange.aspectMask = aspect;
|
|
+ barrier.subresourceRange.baseMipLevel = mipmapLevel;
|
|
+ barrier.subresourceRange.levelCount = 1;
|
|
+ barrier.subresourceRange.baseArrayLayer = slice;
|
|
+ barrier.subresourceRange.layerCount = 1;
|
|
+
|
|
+ DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_NONE, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
+
|
|
+ // now encode the copy
|
|
+ VkBufferImageCopy copyInfo {};
|
|
+ copyInfo.bufferOffset = 0;
|
|
+ copyInfo.bufferRowLength = pitch.rowLength;
|
|
+ copyInfo.bufferImageHeight = pitch.imageHeight;
|
|
+ copyInfo.imageSubresource.aspectMask = aspect;
|
|
+ copyInfo.imageSubresource.mipLevel = mipmapLevel;
|
|
+ copyInfo.imageSubresource.baseArrayLayer = slice;
|
|
+ copyInfo.imageSubresource.layerCount = 1;
|
|
+ copyInfo.imageOffset.x = region.origin.x;
|
|
+ copyInfo.imageOffset.y = region.origin.y;
|
|
+ copyInfo.imageOffset.z = region.origin.z;
|
|
+ copyInfo.imageExtent.width = region.size.width;
|
|
+ copyInfo.imageExtent.height = region.size.height;
|
|
+ copyInfo.imageExtent.depth = region.size.depth;
|
|
+ DynamicVK::vkCmdCopyBufferToImage(cmdBuf, tmpBuf->buffer(), _image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©Info);
|
|
+
|
|
+ // finally, transition the image back to the general layout
|
|
+ barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
+ barrier.newLayout = imageLayout();
|
|
+ barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
+ barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
|
+ DynamicVK::vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
+
|
|
+ DynamicVK::vkEndCommandBuffer(cmdBuf);
|
|
+
|
|
+ VkFenceCreateInfo fenceCreateInfo {};
|
|
+ fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
|
|
+
|
|
+ VkFence theFence = VK_NULL_HANDLE;
|
|
+
|
|
+ if (DynamicVK::vkCreateFence(_device->device(), &fenceCreateInfo, nullptr, &theFence) != VK_SUCCESS) {
|
|
+ // TODO
|
|
+ abort();
|
|
+ }
|
|
|
|
- VkSubmitInfo submitInfo {};
|
|
- submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
|
- submitInfo.commandBufferCount = 1;
|
|
- submitInfo.pCommandBuffers = &cmdBuf;
|
|
+ VkSubmitInfo submitInfo {};
|
|
+ submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
|
|
+ submitInfo.commandBufferCount = 1;
|
|
+ submitInfo.pCommandBuffers = &cmdBuf;
|
|
|
|
- DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence);
|
|
- if (DynamicVK::vkWaitForFences(_device->device(), 1, &theFence, VK_TRUE, /* 1s */ 1ull * 1000 * 1000 * 1000) != VK_SUCCESS) {
|
|
- // TODO
|
|
- abort();
|
|
+ {
|
|
+ flushPendingSubmits(); // same for commits merged into a pending batch (takes the queue mutex itself)
|
|
+ std::lock_guard<std::mutex> queueLock(_device->graphicsQueueMutex());
|
|
+ if (DynamicVK::vkQueueSubmit(_device->graphicsQueue(), 1, &submitInfo, theFence) != VK_SUCCESS) abort();
|
|
+ }
|
|
+ finishOneshot(_device, theFence, cmdBuf, _image, tmpBuf);
|
|
+ poolLock.unlock();
|
|
+ };
|
|
+ if (AsyncEncode::enabled() && !SubmitThread::onWorker()) {
|
|
+ SubmitThread::post(upload);
|
|
+ } else {
|
|
+ upload();
|
|
}
|
|
-
|
|
- DynamicVK::vkDestroyFence(_device->device(), theFence, nullptr);
|
|
-
|
|
- DynamicVK::vkFreeCommandBuffers(_device->device(), _device->oneshotCommandPool(), 1, &cmdBuf);
|
|
};
|
|
diff --git a/src/iridium/air.cpp b/src/iridium/air.cpp
|
|
index 21c501f..9f6663b 100644
|
|
--- a/src/iridium/air.cpp
|
|
+++ b/src/iridium/air.cpp
|
|
@@ -4,9 +4,46 @@
|
|
#include <iridium/dynamic-llvm.hpp>
|
|
|
|
#include <llvm-c/BitReader.h>
|
|
+#include <set>
|
|
+#include <algorithm>
|
|
+#include <functional>
|
|
+#include <llvm/Config/llvm-config.h>
|
|
+
|
|
+// LLVM 23 split the C API's LLVMBr opcode into LLVMUncondBr and LLVMCondBr.
|
|
+#if LLVM_VERSION_MAJOR >= 23
|
|
+#define IRIDIUM_BR_CASES case LLVMUncondBr: case LLVMCondBr
|
|
+#define IRIDIUM_IS_BR(op) ((op) == LLVMUncondBr || (op) == LLVMCondBr)
|
|
+#else
|
|
+#define IRIDIUM_BR_CASES case LLVMBr
|
|
+#define IRIDIUM_IS_BR(op) ((op) == LLVMBr)
|
|
+#endif
|
|
|
|
namespace DynamicLLVM = Iridium::DynamicLLVM;
|
|
|
|
+// AIR links varyings by their user location, not by their order in each stage.
|
|
+// Optimized fragment functions may omit entries that the vertex stage still writes.
|
|
+static uint32_t airInterfaceLocation(const std::vector<LLVMValueRef>& metadata, uint32_t fallback) {
|
|
+ for (size_t i = 0; i < metadata.size(); ++i) {
|
|
+ if (!DynamicLLVM::LLVMIsAMDString(metadata[i])) continue;
|
|
+ unsigned length = 0;
|
|
+ const char* bytes = DynamicLLVM::LLVMGetMDString(metadata[i], &length);
|
|
+ std::string_view value(bytes, length);
|
|
+ constexpr std::string_view prefix = "user(locn";
|
|
+ if (value.size() > prefix.size() + 1 && value.substr(0, prefix.size()) == prefix && value.back() == ')') {
|
|
+ uint32_t location = 0;
|
|
+ for (char digit : value.substr(prefix.size(), value.size() - prefix.size() - 1)) {
|
|
+ if (digit < '0' || digit > '9') throw std::runtime_error("Invalid AIR varying location");
|
|
+ location = location * 10 + static_cast<uint32_t>(digit - '0');
|
|
+ }
|
|
+ return location;
|
|
+ }
|
|
+ if (value == "air.render_target" && i + 1 < metadata.size())
|
|
+ return static_cast<uint32_t>(DynamicLLVM::LLVMConstIntGetZExtValue(metadata[i + 1]));
|
|
+ }
|
|
+ return fallback;
|
|
+}
|
|
+
|
|
+
|
|
// special thanks to https://github.com/YuAo/MetalLibraryArchive for information on the library format
|
|
|
|
// we use LLVM's C API because it's much more stable than the C++ API and it's good enough for our needs
|
|
@@ -29,6 +66,16 @@ Iridium::AIR::Function::Function(Type type, const std::string& name, const void*
|
|
}
|
|
|
|
_module = LLVMSupport::Module(moduleRef, DynamicLLVM::LLVMDisposeModule);
|
|
+ // Normalize local aggregate copies into SSA, then give nested selections and loops
|
|
+ // distinct merges before lowering them into Vulkan's structured control flow.
|
|
+ std::string triple=DynamicLLVM::LLVMGetTarget(moduleRef);
|
|
+ DynamicLLVM::LLVMSetTarget(moduleRef,"spirv64-unknown-unknown");
|
|
+ auto options=DynamicLLVM::LLVMCreatePassBuilderOptions();
|
|
+ auto error=DynamicLLVM::LLVMRunPasses(moduleRef,"function(sroa,mem2reg,loop-unroll,sroa,mem2reg,simplifycfg,structurizecfg)",nullptr,options);
|
|
+ DynamicLLVM::LLVMDisposePassBuilderOptions(options);
|
|
+ DynamicLLVM::LLVMSetTarget(moduleRef,triple.c_str());
|
|
+ if(error){auto message=DynamicLLVM::LLVMGetErrorMessage(error);std::string detail=message?message:"unknown LLVM error";DynamicLLVM::LLVMDisposeErrorMessage(message);throw std::runtime_error("Cannot normalize AIR control flow: "+detail);}
|
|
+
|
|
|
|
_function = DynamicLLVM::LLVMGetNamedFunction(_module.get(), _name.c_str());
|
|
|
|
@@ -41,7 +88,86 @@ Iridium::AIR::Function::Function(Type type, const std::string& name, const void*
|
|
// TEST
|
|
#include <iostream>
|
|
|
|
-class ImpossibleResultID: public std::exception {};
|
|
+static thread_local std::string translationContext;
|
|
+static thread_local LLVMValueRef translationInstruction=nullptr;
|
|
+static std::string describeLLVMValue(LLVMValueRef value) {
|
|
+ char* raw = DynamicLLVM::LLVMPrintValueToString(value);
|
|
+ std::string result = raw ? raw : "unknown value";
|
|
+ if (raw) DynamicLLVM::LLVMDisposeMessage(raw);
|
|
+ return result;
|
|
+}
|
|
+class ImpossibleResultID: public std::runtime_error {
|
|
+public: explicit ImpossibleResultID(const std::string& detail = "Unsupported AIR type"): std::runtime_error(detail) {}
|
|
+};
|
|
+
|
|
+// While set, struct and array types are created without explicit layout, as Private/Function memory needs.
|
|
+static thread_local bool g_layoutFreeTypes = false;
|
|
+
|
|
+class LayoutFreeScope {
|
|
+public:
|
|
+ explicit LayoutFreeScope(bool enabled): _previous(g_layoutFreeTypes) { g_layoutFreeTypes = enabled; }
|
|
+ ~LayoutFreeScope() { g_layoutFreeTypes = _previous; }
|
|
+private:
|
|
+ bool _previous;
|
|
+};
|
|
+
|
|
+// A pointer bitcast retains its intended element type in consuming loads/GEPs.
|
|
+static LLVMTypeRef pointerElementType(LLVMValueRef value, unsigned depth = 0) {
|
|
+ if (depth > 16) throw std::runtime_error("AIR pointer-use chain exceeds limit");
|
|
+ for (auto use = DynamicLLVM::LLVMGetFirstUse(value); use; use = DynamicLLVM::LLVMGetNextUse(use)) {
|
|
+ auto user = DynamicLLVM::LLVMGetUser(use);
|
|
+ if (DynamicLLVM::LLVMGetValueKind(user) != LLVMInstructionValueKind) continue;
|
|
+ auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(user);
|
|
+ if (opcode == LLVMGetElementPtr && DynamicLLVM::LLVMGetOperand(user, 0) == value)
|
|
+ return DynamicLLVM::LLVMGetGEPSourceElementType(user);
|
|
+ if (opcode == LLVMLoad && DynamicLLVM::LLVMGetOperand(user, 0) == value)
|
|
+ return DynamicLLVM::LLVMTypeOf(user);
|
|
+ if ((opcode == LLVMBitCast || opcode == LLVMAddrSpaceCast) && DynamicLLVM::LLVMGetOperand(user, 0) == value)
|
|
+ return pointerElementType(user, depth + 1);
|
|
+ // the pointer stored through carries the stored value's type
|
|
+ if (opcode == LLVMStore && DynamicLLVM::LLVMGetOperand(user, 1) == value)
|
|
+ return DynamicLLVM::LLVMTypeOf(DynamicLLVM::LLVMGetOperand(user, 0));
|
|
+ // a pointer that is merged with others is typed by how the merged pointer is used
|
|
+ if (opcode == LLVMPHI || opcode == LLVMSelect) {
|
|
+ try {
|
|
+ return pointerElementType(user, depth + 1);
|
|
+ } catch (const std::runtime_error&) {
|
|
+ // try the remaining uses
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ // Nothing constrains the pointee (an argument the shader never reads, for example), so any
|
|
+ // 32-bit type describes it as well as another.
|
|
+ return DynamicLLVM::LLVMInt32Type();
|
|
+}
|
|
+// Modern LLVM removes pointee types from pointers. Loads and GEP retain them.
|
|
+static LLVMTypeRef bufferElementType(LLVMValueRef function, size_t parameterIndex) {
|
|
+ auto parameter = DynamicLLVM::LLVMGetParam(function, parameterIndex);
|
|
+ for (auto block = DynamicLLVM::LLVMGetFirstBasicBlock(function); block; block = DynamicLLVM::LLVMGetNextBasicBlock(block)) {
|
|
+ for (auto inst = DynamicLLVM::LLVMGetFirstInstruction(block); inst; inst = DynamicLLVM::LLVMGetNextInstruction(inst)) {
|
|
+ auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(inst);
|
|
+ if (opcode == LLVMGetElementPtr && DynamicLLVM::LLVMGetOperand(inst, 0) == parameter)
|
|
+ return DynamicLLVM::LLVMGetGEPSourceElementType(inst);
|
|
+ if (opcode == LLVMLoad && DynamicLLVM::LLVMGetOperand(inst, 0) == parameter)
|
|
+ return DynamicLLVM::LLVMTypeOf(inst);
|
|
+ }
|
|
+ }
|
|
+ return pointerElementType(parameter);
|
|
+}
|
|
+static LLVMTypeRef gepElementType(LLVMValueRef inst) {
|
|
+ auto type = DynamicLLVM::LLVMGetGEPSourceElementType(inst);
|
|
+ for (unsigned i = 2; i < DynamicLLVM::LLVMGetNumOperands(inst); ++i) {
|
|
+ switch (DynamicLLVM::LLVMGetTypeKind(type)) {
|
|
+ case LLVMStructTypeKind:
|
|
+ type = DynamicLLVM::LLVMStructGetTypeAtIndex(type, DynamicLLVM::LLVMConstIntGetZExtValue(DynamicLLVM::LLVMGetOperand(inst, i)));
|
|
+ break;
|
|
+ case LLVMArrayTypeKind: case LLVMVectorTypeKind:
|
|
+ type = DynamicLLVM::LLVMGetElementType(type); break;
|
|
+ default: throw std::runtime_error("Unsupported AIR GEP element type");
|
|
+ }
|
|
+ }
|
|
+ return type;
|
|
+}
|
|
|
|
static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& builder, LLVMTypeRef llvmType) {
|
|
using namespace Iridium::SPIRV;
|
|
@@ -68,6 +194,10 @@ static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& bui
|
|
return builder.declareType(Type(Type::FloatTag {}, 128));
|
|
|
|
case LLVMIntegerTypeKind:
|
|
+ if (DynamicLLVM::LLVMGetIntTypeWidth(llvmType) == 1) {
|
|
+ // AIR uses i1 for booleans, which SPIR-V has no integer type for
|
|
+ return builder.declareType(Type(Type::BooleanTag {}));
|
|
+ }
|
|
return builder.declareType(Type(Type::IntegerTag {}, DynamicLLVM::LLVMGetIntTypeWidth(llvmType), /* TODO: how to determine this? */ true));
|
|
|
|
case LLVMFunctionTypeKind: {
|
|
@@ -117,28 +247,35 @@ static Iridium::SPIRV::ResultID llvmTypeToSPIRVType(Iridium::SPIRV::Builder& bui
|
|
}
|
|
// align the final offset to determine the structure size
|
|
offset = (offset + (structAlignment - 1)) & ~(structAlignment - 1);
|
|
- return builder.declareType(Type(Type::StructureTag {}, members, offset, structAlignment));
|
|
+ Type structType(Type::StructureTag {}, members, offset, structAlignment);
|
|
+ structType.layoutFree = g_layoutFreeTypes;
|
|
+ return builder.declareType(structType);
|
|
} break;
|
|
|
|
case LLVMPointerTypeKind: {
|
|
StorageClass storageClass = StorageClass::Output;
|
|
// TODO: somehow determine the appropriate storage class
|
|
//auto addrSpace = DynamicLLVM::LLVMGetPointerAddressSpace(llvmType);
|
|
- return builder.declareType(Type(Type::PointerTag {}, storageClass, llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType)), 8));
|
|
+ throw std::runtime_error("Opaque pointer requires instruction-specific element type: " + (translationInstruction?describeLLVMValue(translationInstruction):translationContext));
|
|
} break;
|
|
|
|
case LLVMVectorTypeKind: {
|
|
auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType));
|
|
auto typeInst = *builder.reverseLookupType(type);
|
|
auto elmCount = DynamicLLVM::LLVMGetVectorSize(llvmType);
|
|
- return builder.declareType(Type(Type::VectorTag {}, elmCount, type, typeInst.size * elmCount, ((elmCount == 3 || elmCount == 4) ? 4 : 2) * typeInst.alignment));
|
|
+ // AIR's data layout allocates three-component vectors like four-component vectors.
|
|
+ // This padding belongs to the vector even inside a packed struct or array.
|
|
+ const auto allocatedCount = elmCount == 3 ? 4 : elmCount;
|
|
+ return builder.declareType(Type(Type::VectorTag {}, elmCount, type, typeInst.size * allocatedCount, ((elmCount == 3 || elmCount == 4) ? 4 : 2) * typeInst.alignment));
|
|
} break;
|
|
|
|
case LLVMArrayTypeKind: {
|
|
auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(llvmType));
|
|
auto typeInst = *builder.reverseLookupType(type);
|
|
auto elmCount = DynamicLLVM::LLVMGetArrayLength(llvmType);
|
|
- return builder.declareType(Type(Type::ArrayTag {}, type, elmCount, typeInst.size * elmCount, typeInst.alignment));
|
|
+ Type arrayType(Type::ArrayTag {}, type, elmCount, typeInst.size * elmCount, typeInst.alignment);
|
|
+ arrayType.layoutFree = g_layoutFreeTypes;
|
|
+ return builder.declareType(arrayType);
|
|
} break;
|
|
|
|
default:
|
|
@@ -162,14 +299,60 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
{
|
|
ResultID maybe = builder.lookupResultID(reinterpret_cast<uintptr_t>(llvmValue));
|
|
if (maybe != ResultIDInvalid) {
|
|
+ if (kind == LLVMGlobalVariableValueKind) builder.referenceGlobalVariable(maybe);
|
|
return maybe;
|
|
}
|
|
}
|
|
|
|
+ // LLVM 23 represents vector splats (e.g. `<3 x float> zeroinitializer`) as vector-typed ConstantInt/ConstantFP values
|
|
+ // rather than ConstantAggregateZero/ConstantDataVector. Declare the scalar and splat it into a composite.
|
|
+ if ((kind == LLVMConstantIntValueKind || kind == LLVMConstantFPValueKind) && typeKind == LLVMVectorTypeKind) {
|
|
+ auto elementType = DynamicLLVM::LLVMGetElementType(type);
|
|
+ auto elementKind = DynamicLLVM::LLVMGetTypeKind(elementType);
|
|
+ ResultID element = ResultIDInvalid;
|
|
+ if (kind == LLVMConstantIntValueKind) {
|
|
+ auto val = DynamicLLVM::LLVMConstIntGetZExtValue(llvmValue);
|
|
+ auto width = DynamicLLVM::LLVMGetIntTypeWidth(elementType);
|
|
+ element = width == 1 ? builder.declareConstantBool(val != 0) : ((width <= 32) ? builder.declareConstantScalar<int32_t>(val) : builder.declareConstantScalar<int64_t>(val));
|
|
+ } else {
|
|
+ LLVMBool losesInfo = false;
|
|
+ auto val = DynamicLLVM::LLVMConstRealGetDouble(llvmValue, &losesInfo);
|
|
+ switch (elementKind) {
|
|
+ case LLVMFloatTypeKind: element = builder.declareConstantScalar<float>(val); break;
|
|
+ case LLVMDoubleTypeKind: element = builder.declareConstantScalar<double>(val); break;
|
|
+ case LLVMHalfTypeKind: {
|
|
+ Iridium::Float16 tmp = val;
|
|
+ element = builder.declareConstantScalar<const Iridium::Float16*>(&tmp);
|
|
+ } break;
|
|
+ default:
|
|
+ throw ImpossibleResultID("Unsupported LLVM value: " + describeLLVMValue(llvmValue));
|
|
+ }
|
|
+ }
|
|
+ std::vector<ResultID> vals(DynamicLLVM::LLVMGetVectorSize(type), element);
|
|
+ return builder.declareConstantComposite(llvmTypeToSPIRVType(builder, type), vals);
|
|
+ }
|
|
+
|
|
switch (kind) {
|
|
+ case LLVMGlobalVariableValueKind: {
|
|
+ auto initializer = DynamicLLVM::LLVMGetInitializer(llvmValue);
|
|
+ if (!initializer) throw ImpossibleResultID("External AIR globals are unsupported");
|
|
+ LayoutFreeScope layoutFree(true);
|
|
+ auto valueType = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGlobalGetValueType(llvmValue));
|
|
+ auto pointerType = builder.declareType(Type(Type::PointerTag{}, StorageClass::Private, valueType, 8));
|
|
+ auto initialValue = llvmValueToResultID(builder, initializer);
|
|
+ auto result = builder.addGlobalVariable(pointerType, StorageClass::Private, initialValue);
|
|
+ builder.setResultType(result, pointerType);
|
|
+ builder.associateExistingResultID(result, reinterpret_cast<uintptr_t>(llvmValue));
|
|
+ builder.referenceGlobalVariable(result);
|
|
+ return result;
|
|
+ }
|
|
+
|
|
case LLVMConstantIntValueKind: {
|
|
auto val = DynamicLLVM::LLVMConstIntGetZExtValue(llvmValue);
|
|
auto width = DynamicLLVM::LLVMGetIntTypeWidth(type );
|
|
+ if (width == 1) {
|
|
+ return builder.declareConstantBool(val != 0);
|
|
+ }
|
|
// TODO: how to determine if it's signed or not?
|
|
return (width <= 32) ? builder.declareConstantScalar<int32_t>(val) : builder.declareConstantScalar<int64_t>(val);
|
|
} break;
|
|
@@ -191,13 +374,15 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
} break;
|
|
|
|
default:
|
|
- throw ImpossibleResultID();
|
|
+ throw ImpossibleResultID("Unsupported LLVM value: " + describeLLVMValue(llvmValue));
|
|
}
|
|
} break;
|
|
|
|
- case LLVMConstantVectorValueKind: {
|
|
+ case LLVMConstantStructValueKind:
|
|
+ case LLVMConstantArrayValueKind:
|
|
+ case LLVMConstantVectorValueKind: {
|
|
std::vector<ResultID> vals;
|
|
- auto count = DynamicLLVM::LLVMGetVectorSize(type);
|
|
+ auto count = DynamicLLVM::LLVMGetTypeKind(type)==LLVMStructTypeKind ? DynamicLLVM::LLVMCountStructElementTypes(type) : (DynamicLLVM::LLVMGetTypeKind(type)==LLVMArrayTypeKind ? DynamicLLVM::LLVMGetArrayLength(type) : DynamicLLVM::LLVMGetVectorSize(type));
|
|
|
|
for (size_t i = 0; i < count; ++i) {
|
|
auto constant = DynamicLLVM::LLVMGetOperand(llvmValue, i);
|
|
@@ -207,10 +392,11 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
return builder.declareConstantComposite(llvmTypeToSPIRVType(builder, type), vals);
|
|
} break;
|
|
|
|
- case LLVMConstantDataVectorValueKind: {
|
|
+ case LLVMConstantDataArrayValueKind:
|
|
+ case LLVMConstantDataVectorValueKind: {
|
|
std::vector<ResultID> vals;
|
|
|
|
- auto count = DynamicLLVM::LLVMGetVectorSize(type);
|
|
+ auto count = typeKind == LLVMArrayTypeKind ? DynamicLLVM::LLVMGetArrayLength(type) : DynamicLLVM::LLVMGetVectorSize(type);
|
|
|
|
for (size_t i = 0; i < count; ++i) {
|
|
auto constant = DynamicLLVM::LLVMGetElementAsConstant(llvmValue, i);
|
|
@@ -220,7 +406,8 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
return builder.declareConstantComposite(llvmTypeToSPIRVType(builder, type), vals);
|
|
} break;
|
|
|
|
- case LLVMUndefValueValueKind: {
|
|
+ case LLVMPoisonValueValueKind:
|
|
+ case LLVMUndefValueValueKind: {
|
|
return builder.declareUndefinedValue(llvmTypeToSPIRVType(builder, type));
|
|
} break;
|
|
|
|
@@ -231,6 +418,21 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
|
|
case LLVMConstantExprValueKind: {
|
|
switch (DynamicLLVM::LLVMGetConstOpcode(llvmValue)) {
|
|
+ case LLVMGetElementPtr: {
|
|
+ auto base = llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(llvmValue,0));
|
|
+ auto baseType = *builder.reverseLookupType(builder.lookupResultType(base));
|
|
+ auto firstIndex = DynamicLLVM::LLVMGetOperand(llvmValue,1);
|
|
+ if (DynamicLLVM::LLVMGetValueKind(firstIndex) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(firstIndex) != 0)
|
|
+ throw ImpossibleResultID("Constant AIR GEP requires a zero initial index");
|
|
+ std::vector<ResultID> indices;
|
|
+ for (unsigned i=2;i<DynamicLLVM::LLVMGetNumOperands(llvmValue);++i) indices.push_back(llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(llvmValue,i)));
|
|
+ auto element = llvmTypeToSPIRVType(builder,gepElementType(llvmValue));
|
|
+ auto pointer = builder.declareType(Type(Type::PointerTag{},baseType.pointerStorageClass,element,8));
|
|
+ auto result = indices.empty() ? base : builder.encodeAccessChain(pointer,base,indices);
|
|
+ builder.setResultType(result,pointer);
|
|
+ return result;
|
|
+ }
|
|
+
|
|
case LLVMBitCast: {
|
|
auto target = DynamicLLVM::LLVMGetOperand(llvmValue, 0);
|
|
auto targetID = llvmValueToResultID(builder, target);
|
|
@@ -254,13 +456,315 @@ static Iridium::SPIRV::ResultID llvmValueToResultID(Iridium::SPIRV::Builder& bui
|
|
} break;
|
|
|
|
default:
|
|
- throw ImpossibleResultID();
|
|
+ throw ImpossibleResultID("Unsupported LLVM value: " + describeLLVMValue(llvmValue));
|
|
}
|
|
} break;
|
|
|
|
default:
|
|
- throw ImpossibleResultID();
|
|
+ throw ImpossibleResultID("Unsupported LLVM value: " + describeLLVMValue(llvmValue));
|
|
+ }
|
|
+};
|
|
+
|
|
+
|
|
+static std::vector<std::string_view> splitAirName(std::string_view name) {
|
|
+ std::vector<std::string_view> parts;
|
|
+ for (size_t start = 0; start <= name.size();) {
|
|
+ auto dot = name.find('.', start);
|
|
+ if (dot == std::string_view::npos) {
|
|
+ dot = name.size();
|
|
+ }
|
|
+ parts.push_back(name.substr(start, dot - start));
|
|
+ start = dot + 1;
|
|
+ }
|
|
+ return parts;
|
|
+};
|
|
+
|
|
+// "f32", "f16", "v3f32", ...
|
|
+static bool airTypeTokenIsFloat(std::string_view token) {
|
|
+ if (token.empty()) {
|
|
+ return false;
|
|
}
|
|
+ if (token[0] == 'v') {
|
|
+ token.remove_prefix(1);
|
|
+ while (!token.empty() && token[0] >= '0' && token[0] <= '9') {
|
|
+ token.remove_prefix(1);
|
|
+ }
|
|
+ }
|
|
+ return !token.empty() && token[0] == 'f';
|
|
+};
|
|
+
|
|
+// SPIR-V core opcodes the translation below needs that the Opcode enum does not name
|
|
+namespace SPIRVOp {
|
|
+ constexpr uint16_t ConvertFToU = 109;
|
|
+ constexpr uint16_t ConvertFToS = 110;
|
|
+ constexpr uint16_t ConvertSToF = 111;
|
|
+ constexpr uint16_t ConvertUToF = 112;
|
|
+ constexpr uint16_t UConvert = 113;
|
|
+ constexpr uint16_t SConvert = 114;
|
|
+ constexpr uint16_t FConvert = 115;
|
|
+ constexpr uint16_t Bitcast = 124;
|
|
+ constexpr uint16_t FRem = 140;
|
|
+ constexpr uint16_t Any = 154;
|
|
+ constexpr uint16_t All = 155;
|
|
+ constexpr uint16_t Select = 169;
|
|
+ constexpr uint16_t IEqual = 170;
|
|
+ constexpr uint16_t INotEqual = 171;
|
|
+ constexpr uint16_t UGreaterThan = 172;
|
|
+ constexpr uint16_t SGreaterThan = 173;
|
|
+ constexpr uint16_t UGreaterThanEqual = 174;
|
|
+ constexpr uint16_t SGreaterThanEqual = 175;
|
|
+ constexpr uint16_t ULessThan = 176;
|
|
+ constexpr uint16_t SLessThan = 177;
|
|
+ constexpr uint16_t ULessThanEqual = 178;
|
|
+ constexpr uint16_t SLessThanEqual = 179;
|
|
+ constexpr uint16_t FOrdEqual = 180;
|
|
+ constexpr uint16_t FUnordEqual = 181;
|
|
+ constexpr uint16_t FOrdNotEqual = 182;
|
|
+ constexpr uint16_t FUnordNotEqual = 183;
|
|
+ constexpr uint16_t FOrdLessThan = 184;
|
|
+ constexpr uint16_t FUnordLessThan = 185;
|
|
+ constexpr uint16_t FOrdGreaterThan = 186;
|
|
+ constexpr uint16_t FUnordGreaterThan = 187;
|
|
+ constexpr uint16_t FOrdLessThanEqual = 188;
|
|
+ constexpr uint16_t FUnordLessThanEqual = 189;
|
|
+ constexpr uint16_t FOrdGreaterThanEqual = 190;
|
|
+ constexpr uint16_t FUnordGreaterThanEqual = 191;
|
|
+ constexpr uint16_t Ordered = 162;
|
|
+ constexpr uint16_t Unordered = 163;
|
|
+ constexpr uint16_t DPdx = 207;
|
|
+ constexpr uint16_t DPdy = 208;
|
|
+ constexpr uint16_t Fwidth = 209;
|
|
+ constexpr uint16_t Kill = 252;
|
|
+};
|
|
+
|
|
+static bool translateAirTexture(Iridium::SPIRV::Builder& builder, LLVMValueRef inst, std::string_view name, Iridium::SPIRV::ResultID resultType, Iridium::FunctionType stage, Iridium::SPIRV::ResultID& result) {
|
|
+ using namespace Iridium::SPIRV;
|
|
+ bool compare = name.find("air.sample_compare_depth_") == 0;
|
|
+ bool sample = name.find("air.sample_texture_") == 0 || compare;
|
|
+ bool read = name.find("air.read_texture_") == 0;
|
|
+ bool write = name.find("air.write_texture_") == 0;
|
|
+ bool width = name == "air.get_width_texture_2d", height = name == "air.get_height_texture_2d";
|
|
+ if (!sample && !read && !write && !width && !height) return false;
|
|
+ auto operand = [&](unsigned i) { return llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, i)); };
|
|
+ auto texturePointer = operand(0);
|
|
+ auto pointerType = *builder.reverseLookupType(builder.lookupResultType(texturePointer));
|
|
+ auto imageTypeID = pointerType.targetType;
|
|
+ auto imageType = *builder.reverseLookupType(imageTypeID);
|
|
+ auto image = builder.encodeLoad(imageTypeID, texturePointer);
|
|
+ unsigned dims = imageType.imageDimensionality == Dim::e1D ? 1 : (imageType.imageDimensionality == Dim::e2D ? 2 : 3);
|
|
+ bool array = imageType.imageIsArrayed;
|
|
+ auto llvmResult = DynamicLLVM::LLVMTypeOf(inst);
|
|
+ bool wrapped = DynamicLLVM::LLVMGetTypeKind(llvmResult) == LLVMStructTypeKind;
|
|
+ auto valueTypeID = wrapped ? llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMStructGetTypeAtIndex(llvmResult,0)) : resultType;
|
|
+ auto finish = [&](ResultID value) {
|
|
+ if (!wrapped) { result=value; return; }
|
|
+ auto partial=builder.encodeCompositeInsert(resultType,value,builder.declareUndefinedValue(resultType),{0});
|
|
+ result=builder.encodeCompositeInsert(resultType,builder.declareConstantScalar<int8_t>(0),partial,{1});
|
|
+ };
|
|
+ if (width || height) {
|
|
+ builder.requireCapability(Capability::ImageQuery);
|
|
+ auto component=builder.declareType(Type(Type::IntegerTag{},32,true));
|
|
+ auto sizeType=builder.declareType(Type(Type::VectorTag{},dims+(array?1:0),component,4*(dims+(array?1:0)),4));
|
|
+ auto sizes=builder.encodeRawOp(103,sizeType,{image,operand(1)}); // OpImageQuerySizeLod
|
|
+ result=builder.encodeCompositeExtract(resultType,sizes,{height?1u:0u});return true;
|
|
+ }
|
|
+ auto coordinates = operand(compare?3:(sample?2:1));
|
|
+ auto coordinateType = *builder.reverseLookupType(builder.lookupResultType(coordinates));
|
|
+ if (array) {
|
|
+ std::vector<ResultID> components;
|
|
+ for(unsigned i=0;i<dims;++i) components.push_back(builder.encodeCompositeExtract(coordinateType.targetType,coordinates,{i}));
|
|
+ auto layer=operand(sample?3:2);
|
|
+ if(sample) layer=builder.encodeRawOp(111,coordinateType.targetType,{layer}); // OpConvertSToF
|
|
+ components.push_back(layer);
|
|
+ auto composite=builder.declareType(Type(Type::VectorTag{},dims+1,coordinateType.targetType,(dims+1)*4,4));
|
|
+ coordinates=builder.encodeRawOp(80,composite,components);
|
|
+ }
|
|
+ auto vectorType=builder.declareType(Type(Type::VectorTag{},4,imageType.targetType,16,16));
|
|
+ ResultID value;
|
|
+ if(sample) {
|
|
+ auto samplerPointer=operand(1);
|
|
+ auto samplerType=*builder.reverseLookupType(builder.lookupResultType(samplerPointer));
|
|
+ auto sampler=builder.encodeLoad(samplerType.targetType,samplerPointer);
|
|
+ auto sampledType=builder.declareType(Type(Type::SampledImageTag{},imageTypeID));
|
|
+ auto sampled=builder.encodeSampledImage(sampledType,image,sampler);
|
|
+ unsigned extra=array?1:0;
|
|
+ bool cube=imageType.imageDimensionality==Dim::eCube;
|
|
+ unsigned flagIndex=compare?(cube?5:7):(cube?3+extra:5+extra);
|
|
+ auto lodFlag=DynamicLLVM::LLVMGetOperand(inst,flagIndex);
|
|
+ if(DynamicLLVM::LLVMGetValueKind(lodFlag)!=LLVMConstantIntValueKind)
|
|
+ throw std::runtime_error("Dynamic AIR sample LOD mode: " + describeLLVMValue(inst));
|
|
+ bool explicitLod=DynamicLLVM::LLVMConstIntGetZExtValue(lodFlag)!=0 || stage!=Iridium::FunctionType::Fragment;
|
|
+ std::vector<ResultID> args{sampled,coordinates};
|
|
+ if(compare)args.push_back(operand(4));
|
|
+ auto offset=cube?nullptr:DynamicLLVM::LLVMGetOperand(inst,flagIndex-1);
|
|
+ bool hasOffset=offset && !DynamicLLVM::LLVMIsNull(offset);
|
|
+ unsigned mask=(explicitLod?2:0)|(hasOffset?8:0);
|
|
+ if(mask){args.push_back(mask);if(explicitLod)args.push_back(operand(flagIndex+1));if(hasOffset)args.push_back(llvmValueToResultID(builder,offset));}
|
|
+ value=builder.encodeRawOp(compare?(explicitLod?90:89):(explicitLod?88:87),compare?valueTypeID:vectorType,args);
|
|
+ if(compare){finish(value);return true;}
|
|
+ } else if(read) {
|
|
+ std::vector<ResultID> args{image,coordinates};
|
|
+ if(imageType.imageIsSampledIndication==1){args.push_back(2);args.push_back(operand(array?3:2));}
|
|
+ value=builder.encodeRawOp(imageType.imageIsSampledIndication==1?95:98,vectorType,args);
|
|
+ } else {
|
|
+ builder.encodeRawOpNoResult(99,{image,coordinates,operand(array?3:2)});result=ResultIDInvalid;return true;
|
|
+ }
|
|
+ if(vectorType!=valueTypeID) {
|
|
+ auto target=*builder.reverseLookupType(valueTypeID);
|
|
+ auto component=*builder.reverseLookupType(target.targetType);
|
|
+ value=component.backingType==Type::BackingType::FloatingPoint && component.scalarWidth==16 ? builder.encodeFConvert(valueTypeID,value) : builder.encodeBitcast(valueTypeID,value);
|
|
+ }
|
|
+ finish(value);return true;
|
|
+}
|
|
+
|
|
+// LLVM integers are signless; SPIR-V conversion instructions require explicit signedness.
|
|
+static Iridium::SPIRV::ResultID integerTypeWithSign(Iridium::SPIRV::Builder& builder,Iridium::SPIRV::ResultID type,bool sign){
|
|
+ using namespace Iridium::SPIRV;auto value=*builder.reverseLookupType(type);
|
|
+ if(value.backingType==Type::BackingType::Vector){auto component=integerTypeWithSign(builder,value.targetType,sign);return builder.declareType(Type(Type::VectorTag{},value.entryCount,component,value.size,value.alignment));}
|
|
+ return builder.declareType(Type(Type::IntegerTag{},value.scalarWidth,sign));
|
|
+}
|
|
+// Translates the AIR math, conversion, derivative and discard intrinsics. Returns false when `name`
|
|
+// is not one of them. `terminatesBlock` is set for instructions that end the SPIR-V block (discard).
|
|
+static bool translateAirIntrinsic(Iridium::SPIRV::Builder& builder, LLVMValueRef inst, std::string_view name, Iridium::SPIRV::ResultID type, Iridium::SPIRV::ResultID& resID, bool& terminatesBlock) {
|
|
+ using namespace Iridium::SPIRV;
|
|
+
|
|
+ auto operand = [&](unsigned index) {
|
|
+ return llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, index));
|
|
+ };
|
|
+
|
|
+ if (name == "air.discard_fragment") {
|
|
+ builder.encodeRawOpNoResult(SPIRVOp::Kill, {});
|
|
+ terminatesBlock = true;
|
|
+ resID = ResultIDInvalid;
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ auto parts = splitAirName(name);
|
|
+ if (parts.size() < 3 || parts[0] != "air") {
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ std::string_view fn = parts[1];
|
|
+ if (fn.substr(0, 5) == "fast_") {
|
|
+ fn.remove_prefix(5);
|
|
+ } else if (fn.substr(0, 8) == "precise_") {
|
|
+ fn.remove_prefix(8);
|
|
+ }
|
|
+
|
|
+ // floating-point functions: air.<fn>.<type>
|
|
+ if (parts.size() == 3 && airTypeTokenIsFloat(parts[2])) {
|
|
+ struct MathOp {
|
|
+ std::string_view name;
|
|
+ GLSLOpcode opcode;
|
|
+ unsigned arity;
|
|
+ };
|
|
+ static const MathOp mathOps[] = {
|
|
+ { "sqrt", GLSLOpcode::Sqrt, 1 }, { "rsqrt", GLSLOpcode::InverseSqrt, 1 }, { "pow", GLSLOpcode::Pow, 2 },
|
|
+ { "powr", GLSLOpcode::Pow, 2 }, { "exp", GLSLOpcode::Exp, 1 }, { "exp2", GLSLOpcode::Exp2, 1 },
|
|
+ { "log", GLSLOpcode::Log, 1 }, { "log2", GLSLOpcode::Log2, 1 }, { "sin", GLSLOpcode::Sin, 1 },
|
|
+ { "cos", GLSLOpcode::Cos, 1 }, { "tan", GLSLOpcode::Tan, 1 }, { "asin", GLSLOpcode::Asin, 1 },
|
|
+ { "acos", GLSLOpcode::Acos, 1 }, { "atan", GLSLOpcode::Atan, 1 }, { "atan2", GLSLOpcode::Atan2, 2 },
|
|
+ { "sinh", GLSLOpcode::Sinh, 1 }, { "cosh", GLSLOpcode::Cosh, 1 }, { "tanh", GLSLOpcode::Tanh, 1 },
|
|
+ { "fabs", GLSLOpcode::FAbs, 1 }, { "floor", GLSLOpcode::Floor, 1 }, { "ceil", GLSLOpcode::Ceil, 1 },
|
|
+ { "fract", GLSLOpcode::Fract, 1 }, { "rint", GLSLOpcode::RoundEven, 1 }, { "round", GLSLOpcode::Round, 1 },
|
|
+ { "trunc", GLSLOpcode::Trunc, 1 }, { "sign", GLSLOpcode::FSign, 1 }, { "fmin", GLSLOpcode::FMin, 2 },
|
|
+ { "fmax", GLSLOpcode::FMax, 2 }, { "fma", GLSLOpcode::Fma, 3 }, { "mix", GLSLOpcode::FMix, 3 },
|
|
+ { "clamp", GLSLOpcode::FClamp, 3 }, { "step", GLSLOpcode::Step, 2 }, { "smoothstep", GLSLOpcode::SmoothStep, 3 },
|
|
+ { "normalize", GLSLOpcode::Normalize, 1 }, { "length", GLSLOpcode::Length, 1 }, { "distance", GLSLOpcode::Distance, 2 },
|
|
+ { "cross", GLSLOpcode::Cross, 2 }, { "reflect", GLSLOpcode::Reflect, 2 },
|
|
+ };
|
|
+
|
|
+ for (auto& op: mathOps) {
|
|
+ if (op.name == fn) {
|
|
+ std::vector<ResultID> arguments;
|
|
+ for (unsigned i = 0; i < op.arity; ++i) {
|
|
+ arguments.push_back(operand(i));
|
|
+ }
|
|
+ resID = builder.encodeGLSLOp(op.opcode, type, arguments);
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (fn == "fmod") {
|
|
+ resID = builder.encodeRawOp(SPIRVOp::FRem, type, { operand(0), operand(1) });
|
|
+ return true;
|
|
+ }
|
|
+ if (fn == "dot") {
|
|
+ resID = builder.encodeArithBinop(Opcode::Dot, type, operand(0), operand(1));
|
|
+ return true;
|
|
+ }
|
|
+ if (fn == "dfdx" || fn == "dfdy" || fn == "fwidth") {
|
|
+ resID = builder.encodeRawOp(fn == "dfdx" ? SPIRVOp::DPdx : (fn == "dfdy" ? SPIRVOp::DPdy : SPIRVOp::Fwidth), type, { operand(0) });
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // boolean vector reductions: air.all.v2i1, air.any.v4i1
|
|
+ if (parts.size() == 3 && (fn == "all" || fn == "any")) {
|
|
+ resID = builder.encodeRawOp(fn == "all" ? SPIRVOp::All : SPIRVOp::Any, type, { operand(0) });
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ // integer functions: air.<fn>.<s|u>.<type>
|
|
+ if (parts.size() == 4 && (parts[2] == "s" || parts[2] == "u")) {
|
|
+ bool isSigned = parts[2] == "s";
|
|
+
|
|
+ if (fn == "min" || fn == "max") {
|
|
+ auto opcode = fn == "min" ? (isSigned ? GLSLOpcode::SMin : GLSLOpcode::UMin) : (isSigned ? GLSLOpcode::SMax : GLSLOpcode::UMax);
|
|
+ resID = builder.encodeGLSLOp(opcode, type, { operand(0), operand(1) });
|
|
+ return true;
|
|
+ }
|
|
+ if (fn == "clamp") {
|
|
+ resID = builder.encodeGLSLOp(isSigned ? GLSLOpcode::SClamp : GLSLOpcode::UClamp, type, { operand(0), operand(1), operand(2) });
|
|
+ return true;
|
|
+ }
|
|
+ if (fn == "abs") {
|
|
+ resID = builder.encodeGLSLOp(GLSLOpcode::SAbs, type, { operand(0) });
|
|
+ return true;
|
|
+ }
|
|
+ return false;
|
|
+ }
|
|
+
|
|
+ // conversions: air.convert.<dst kind>.<dst type>.<src kind>.<src type>
|
|
+ if (parts.size() == 6 && fn == "convert") {
|
|
+ auto dstKind = parts[2];
|
|
+ auto dstType = parts[3];
|
|
+ auto srcKind = parts[4];
|
|
+ auto srcType = parts[5];
|
|
+ auto argument = operand(0);
|
|
+
|
|
+ if (srcType == "i1") {
|
|
+ // bool to number: 1 or 0
|
|
+ auto one = dstKind == "f" ? builder.declareConstantScalar<float>(1) : builder.declareConstantScalar<int32_t>(1);
|
|
+ auto zero = dstKind == "f" ? builder.declareConstantScalar<float>(0) : builder.declareConstantScalar<int32_t>(0);
|
|
+ resID = builder.encodeRawOp(SPIRVOp::Select, type, { argument, one, zero });
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ if (dstKind == "f" && srcKind == "s") {
|
|
+ resID = builder.encodeRawOp(SPIRVOp::ConvertSToF, type, { argument });
|
|
+ } else if (dstKind == "f" && srcKind == "u") {
|
|
+ auto argType=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(DynamicLLVM::LLVMGetOperand(inst,0)));
|
|
+ auto unsignedType=integerTypeWithSign(builder,argType,false);
|
|
+ resID=builder.encodeRawOp(SPIRVOp::ConvertUToF,type,{builder.encodeBitcast(unsignedType,argument)});
|
|
+ } else if (dstKind == "s" && srcKind == "f") {
|
|
+ resID = builder.encodeRawOp(SPIRVOp::ConvertFToS, type, { argument });
|
|
+ } else if (dstKind == "u" && srcKind == "f") {
|
|
+ auto unsignedType=integerTypeWithSign(builder,type,false);
|
|
+ resID=builder.encodeBitcast(type,builder.encodeRawOp(SPIRVOp::ConvertFToU,unsignedType,{argument}));
|
|
+ } else if (dstKind == "f" && srcKind == "f") {
|
|
+ // same type: nothing to convert
|
|
+ resID = (dstType == srcType) ? argument : builder.encodeRawOp(SPIRVOp::FConvert, type, { argument });
|
|
+ } else if ((dstKind == "s" || dstKind == "u") && (srcKind == "s" || srcKind == "u")) {
|
|
+ resID = (dstType == srcType) ? argument : builder.encodeRawOp(dstKind == "s" ? SPIRVOp::SConvert : SPIRVOp::UConvert, type, { argument });
|
|
+ } else {
|
|
+ return false;
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ return false;
|
|
};
|
|
|
|
void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& outputInfo) {
|
|
@@ -312,7 +816,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
});
|
|
|
|
// declare the GLSL PerVertex structure
|
|
- auto perVertexStructType = builder.declareType(SPIRV::Type(SPIRV::Type::StructureTag {}, {
|
|
+ SPIRV::Type perVertexType(SPIRV::Type::StructureTag {}, {
|
|
SPIRV::Type::Member {
|
|
vec4Type,
|
|
0,
|
|
@@ -321,13 +825,21 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
}
|
|
},
|
|
// TODO: declare the rest of the structure
|
|
- }, floatTypeInst.size * 4, floatTypeInst.alignment));
|
|
+ }, floatTypeInst.size * 4, floatTypeInst.alignment);
|
|
+ perVertexType.layoutFree = true;
|
|
+ auto perVertexStructType = builder.declareType(perVertexType);
|
|
auto perVertexStructPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, perVertexStructType, 8));
|
|
perVertexVar = builder.addGlobalVariable(perVertexStructPtrType, SPIRV::StorageClass::Output);
|
|
|
|
builder.addDecoration(perVertexStructType, SPIRV::Decoration { SPIRV::DecorationType::Block });
|
|
|
|
builder.referenceGlobalVariable(perVertexVar);
|
|
+ // Indium also creates a point-topology pipeline; Vulkan requires PointSize to be written.
|
|
+ auto pointSizePtr = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, floatType, 8));
|
|
+ auto pointSizeVar = builder.addGlobalVariable(pointSizePtr, SPIRV::StorageClass::Output);
|
|
+ builder.addDecoration(pointSizeVar, SPIRV::Decoration { SPIRV::DecorationType::Builtin, { static_cast<uint32_t>(SPIRV::BuiltinID::PointSize) } });
|
|
+ builder.referenceGlobalVariable(pointSizeVar);
|
|
+ builder.encodeStore(pointSizeVar, builder.declareConstantScalar<float>(1.0f));
|
|
|
|
// declare the GLSL VertexIndex input variable
|
|
vertexIndexVar = builder.addGlobalVariable(ptrIntType, SPIRV::StorageClass::Input);
|
|
@@ -425,7 +937,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
// if the function returns a structure, we need to separate the components
|
|
// into separate output variables.
|
|
|
|
- auto llfuncType = DynamicLLVM::LLVMGetElementType(DynamicLLVM::LLVMTypeOf(_function));
|
|
+ auto llfuncType = DynamicLLVM::LLVMGlobalGetValueType(_function);
|
|
auto funcRetType = DynamicLLVM::LLVMGetReturnType(llfuncType);
|
|
std::vector<LLVMTypeRef> funcParamTypes(DynamicLLVM::LLVMCountParamTypes(llfuncType));
|
|
DynamicLLVM::LLVMGetParamTypes(llfuncType, funcParamTypes.data());
|
|
@@ -460,7 +972,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMStructGetTypeAtIndex(funcRetType, i));
|
|
auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, type, 8));
|
|
auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Output);
|
|
- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { location } });
|
|
+ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { airInterfaceLocation(returnValueMemberInfo, location) } });
|
|
++location;
|
|
_outputValueIDs.push_back(var);
|
|
builder.referenceGlobalVariable(var);
|
|
@@ -472,13 +984,60 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto type = llvmTypeToSPIRVType(builder, funcRetType);
|
|
auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Output, type, 8));
|
|
auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Output);
|
|
- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { 0 } });
|
|
+ uint32_t outputLocation = 0;
|
|
+ if (!returnValueOperands.empty()) {
|
|
+ std::vector<LLVMValueRef> info(DynamicLLVM::LLVMGetMDNodeNumOperands(returnValueOperands[0]));
|
|
+ DynamicLLVM::LLVMGetMDNodeOperands(returnValueOperands[0], info.data());
|
|
+ outputLocation = airInterfaceLocation(info, 0);
|
|
+ }
|
|
+ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { outputLocation } });
|
|
_outputValueIDs.push_back(var);
|
|
builder.referenceGlobalVariable(var);
|
|
}
|
|
|
|
std::vector<LLVMValueRef> parameterOperands(DynamicLLVM::LLVMGetMDNodeNumOperands(rootInfoOperands[2]));
|
|
DynamicLLVM::LLVMGetMDNodeOperands(rootInfoOperands[2], parameterOperands.data());
|
|
+ // Preserve AIR reflection metadata before translating resource bindings.
|
|
+ for (auto parameter : parameterOperands) {
|
|
+ std::vector<LLVMValueRef> info(DynamicLLVM::LLVMGetMDNodeNumOperands(parameter));
|
|
+ DynamicLLVM::LLVMGetMDNodeOperands(parameter, info.data());
|
|
+ if (info.size() < 2) continue;
|
|
+ auto kind = llvmMDStringToStringView(info[1]);
|
|
+ ArgumentInfo arg;
|
|
+ if (kind == "air.buffer") arg.type = BindingType::Buffer;
|
|
+ else if (kind == "air.texture") arg.type = BindingType::Texture;
|
|
+ else if (kind == "air.sampler") arg.type = BindingType::Sampler;
|
|
+ else continue;
|
|
+ for (size_t i = 2; i + 1 < info.size(); ++i) {
|
|
+ if (!DynamicLLVM::LLVMIsAMDString(info[i])) continue;
|
|
+ auto key = llvmMDStringToStringView(info[i]);
|
|
+ if (key == "air.location_index") arg.index = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]);
|
|
+ else if (key == "air.arg_name") arg.name = llvmMDStringToStringView(info[i+1]);
|
|
+ else if (key == "air.arg_type_name") arg.typeName = llvmMDStringToStringView(info[i+1]);
|
|
+ else if (key == "air.arg_type_size") arg.size = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]);
|
|
+ else if (key == "air.arg_type_align_size") arg.alignment = DynamicLLVM::LLVMConstIntGetZExtValue(info[i+1]);
|
|
+ else if (key == "air.struct_type_info") {
|
|
+ std::vector<LLVMValueRef> members(DynamicLLVM::LLVMGetMDNodeNumOperands(info[i+1]));
|
|
+ DynamicLLVM::LLVMGetMDNodeOperands(info[i+1], members.data());
|
|
+ // AIR stores each direct member as offset, size, array length, type, name. A member of
|
|
+ // struct type is preceded by an "air.struct_type_info" key and the node describing it.
|
|
+ // Reflection is informational, so anything unrecognized stops the walk instead of failing.
|
|
+ for (size_t j = 0; j < members.size();) {
|
|
+ if (DynamicLLVM::LLVMIsAMDString(members[j])) {
|
|
+ j += 2;
|
|
+ continue;
|
|
+ }
|
|
+ if (j + 4 >= members.size() || !DynamicLLVM::LLVMIsAMDString(members[j+3]) || !DynamicLLVM::LLVMIsAMDString(members[j+4]))
|
|
+ break;
|
|
+ arg.members.push_back({static_cast<size_t>(DynamicLLVM::LLVMConstIntGetZExtValue(members[j])),
|
|
+ std::string(llvmMDStringToStringView(members[j+3])), std::string(llvmMDStringToStringView(members[j+4]))});
|
|
+ j += 5;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ funcInfo.arguments.push_back(std::move(arg));
|
|
+ }
|
|
+
|
|
|
|
//
|
|
// analyze parameters and mark special values (like the vertex ID)
|
|
@@ -519,7 +1078,9 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto kind = llvmMDStringToStringView(parameterInfo[1]);
|
|
|
|
if (kind == "air.buffer") {
|
|
- auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMGetElementType(funcParamTypes[i]));
|
|
+ translationInstruction=nullptr;
|
|
+ translationContext = "buffer parameter " + std::to_string(i) + " of " + _name;
|
|
+ auto type = llvmTypeToSPIRVType(builder, bufferElementType(_function, i));
|
|
auto addrPtrTypeInst = SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::PhysicalStorageBuffer, type, 8);
|
|
auto addrPtrType = builder.declareType(addrPtrTypeInst);
|
|
bufferMembers.push_back(SPIRV::Type::Member { addrPtrType, 8 * bufferIndex, {} });
|
|
@@ -575,7 +1136,21 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
_parameterIDs.push_back(load);
|
|
builder.associateExistingResultID(load, llparamVal);
|
|
builder.setResultType(load, intType);
|
|
- } else if (kind == "air.buffer") {
|
|
+ } else if (kind == "air.instance_id") {
|
|
+ auto var=builder.addGlobalVariable(ptrIntType,SPIRV::StorageClass::Input);
|
|
+ builder.addDecoration(var,SPIRV::Decoration{SPIRV::DecorationType::Builtin,{43}});builder.referenceGlobalVariable(var);
|
|
+ auto load=builder.encodeLoad(intType,var);_parameterIDs.push_back(load);builder.associateExistingResultID(load,llparamVal);builder.setResultType(load,intType);
|
|
+ } else if (kind == "air.front_facing") {
|
|
+ auto boolType = builder.declareType(SPIRV::Type(SPIRV::Type::BooleanTag{}));
|
|
+ auto boolPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{}, SPIRV::StorageClass::Input, boolType, 8));
|
|
+ auto var = builder.addGlobalVariable(boolPtrType, SPIRV::StorageClass::Input);
|
|
+ builder.addDecoration(var, SPIRV::Decoration{SPIRV::DecorationType::Builtin, {17}}); // FrontFacing
|
|
+ builder.referenceGlobalVariable(var);
|
|
+ auto load = builder.encodeLoad(boolType, var);
|
|
+ _parameterIDs.push_back(load);
|
|
+ builder.associateExistingResultID(load, llparamVal);
|
|
+ builder.setResultType(load, boolType);
|
|
+ } else if (kind == "air.buffer") {
|
|
// find the location index info
|
|
size_t infoIdx = 0;
|
|
for (; infoIdx < parameterInfo.size(); ++infoIdx) {
|
|
@@ -620,7 +1195,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Input);
|
|
auto load = builder.encodeLoad(type, var);
|
|
|
|
- builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { paramLocation } });
|
|
+ builder.addDecoration(var, SPIRV::Decoration { SPIRV::DecorationType::Location, { airInterfaceLocation(parameterInfo, paramLocation) } });
|
|
++paramLocation;
|
|
|
|
builder.referenceGlobalVariable(var);
|
|
@@ -631,9 +1206,18 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
builder.setResultType(load, type);
|
|
} else if (kind == "air.vertex_input") {
|
|
auto type = llvmTypeToSPIRVType(builder, funcParamTypes[i]);
|
|
- auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Input, type, 8));
|
|
+ auto inputType = type;
|
|
+ // LLVM integers have no sign; AIR metadata preserves the interface's unsigned type.
|
|
+ for (size_t j = 0; j + 1 < parameterInfo.size(); ++j) {
|
|
+ if (llvmMDStringToStringView(parameterInfo[j]) != "air.arg_type_name") continue;
|
|
+ auto name = llvmMDStringToStringView(parameterInfo[j + 1]);
|
|
+ if (name.substr(0, 4) == "uint" || name.substr(0, 6) == "ushort" || name.substr(0, 5) == "uchar" || name.substr(0, 5) == "ulong")
|
|
+ inputType = integerTypeWithSign(builder, type, false);
|
|
+ }
|
|
+ auto ptrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::Input, inputType, 8));
|
|
auto var = builder.addGlobalVariable(ptrType, SPIRV::StorageClass::Input);
|
|
- auto load = builder.encodeLoad(type, var);
|
|
+ auto load = builder.encodeLoad(inputType, var);
|
|
+ if (inputType != type) load = builder.encodeBitcast(type, load);
|
|
|
|
// find the location index info
|
|
size_t infoIdx = 0;
|
|
@@ -761,14 +1345,19 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
realSampleType = fakeSampleType;
|
|
}
|
|
|
|
- // TODO: same here (find a better way...)
|
|
- if (textureClassName == "texture2d") {
|
|
- dimensionality = SPIRV::Dim::e2D;
|
|
- } else if (textureClassName == "texturecube") {
|
|
- dimensionality = SPIRV::Dim::eCube;
|
|
- }
|
|
-
|
|
- auto imageType = builder.declareType(SPIRV::Type(SPIRV::Type::ImageTag {}, fakeSampleType, realSampleType, dimensionality, 2, false, false, accessType == TextureAccessType::Sample ? 1 : 2, SPIRV::ImageFormat::Unknown));
|
|
+ if (sampleTypeName == "uint" || sampleTypeName == "int") {
|
|
+ fakeSampleType = builder.declareType(SPIRV::Type(SPIRV::Type::IntegerTag {},32,sampleTypeName=="int"));
|
|
+ realSampleType = fakeSampleType;
|
|
+ }
|
|
+ if (fakeSampleType == SPIRV::ResultIDInvalid) throw std::runtime_error("Unsupported AIR texture sample type");
|
|
+ bool arrayed = textureClassName.find("_array") != std::string_view::npos;
|
|
+ if ((textureClassName.find("texture2d") == 0 || textureClassName.find("depth2d") == 0)) dimensionality = SPIRV::Dim::e2D;
|
|
+ else if ((textureClassName.find("texturecube") == 0 || textureClassName.find("depthcube") == 0)) dimensionality = SPIRV::Dim::eCube;
|
|
+ else if (textureClassName == "texture3d") dimensionality = SPIRV::Dim::e3D;
|
|
+ else if (textureClassName == "texture1d") dimensionality = SPIRV::Dim::e1D;
|
|
+ else throw std::runtime_error("Unsupported AIR texture dimension: " + std::string(textureClassName));
|
|
+
|
|
+ auto imageType = builder.declareType(SPIRV::Type(SPIRV::Type::ImageTag {}, fakeSampleType, realSampleType, dimensionality, textureClassName.find("depth")==0?1:0, arrayed, false, accessType == TextureAccessType::Sample ? 1 : 2, SPIRV::ImageFormat::Unknown));
|
|
auto imagePtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::UniformConstant, imageType, 8));
|
|
auto var = builder.addGlobalVariable(imagePtrType, SPIRV::StorageClass::UniformConstant);
|
|
//auto load = builder.encodeLoad(imageType, var);
|
|
@@ -959,68 +1548,98 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
enum class CFGType {
|
|
None = 0,
|
|
Selection,
|
|
+ Loop,
|
|
};
|
|
|
|
struct CFGInfo {
|
|
CFGType type = CFGType::None;
|
|
SPIRV::ResultID selectionMergeBlock = SPIRV::ResultIDInvalid;
|
|
+ SPIRV::ResultID continueBlock = SPIRV::ResultIDInvalid;
|
|
};
|
|
|
|
std::unordered_map<SPIRV::ResultID, CFGInfo> cfgInfos;
|
|
|
|
- for (LLVMBasicBlockRef bb = DynamicLLVM::LLVMGetFirstBasicBlock(_function); bb != nullptr; bb = DynamicLLVM::LLVMGetNextBasicBlock(bb)) {
|
|
- auto lastInst = DynamicLLVM::LLVMGetLastInstruction(bb);
|
|
- auto lastInstOpcode = DynamicLLVM::LLVMGetInstructionOpcode(lastInst);
|
|
-
|
|
- CFGInfo cfgInfo {};
|
|
-
|
|
- if (lastInstOpcode == LLVMBr && DynamicLLVM::LLVMIsConditional(lastInst)) {
|
|
- // check if this is a basic conditional/selection (i.e. an `if`)
|
|
- auto firstBlock = DynamicLLVM::LLVMGetSuccessor(lastInst, 0);
|
|
- auto secondBlock = DynamicLLVM::LLVMGetSuccessor(lastInst, 1);
|
|
-
|
|
- auto lastInstFirstBlock = DynamicLLVM::LLVMGetLastInstruction(firstBlock);
|
|
- auto lastInstSecondBlock = DynamicLLVM::LLVMGetLastInstruction(secondBlock);
|
|
- auto lastInstOpcodeFirstBlock = DynamicLLVM::LLVMGetInstructionOpcode(lastInstFirstBlock);
|
|
- auto lastInstOpcodeSecondBlock = DynamicLLVM::LLVMGetInstructionOpcode(lastInstSecondBlock);
|
|
-
|
|
- SPIRV::ResultID mergeBlock = SPIRV::ResultIDInvalid;
|
|
-
|
|
- // check if the first block is the special block
|
|
- if (lastInstOpcodeFirstBlock == LLVMBr && !DynamicLLVM::LLVMIsConditional(lastInstFirstBlock)) {
|
|
- // make sure the target block is the same
|
|
- auto block = DynamicLLVM::LLVMGetSuccessor(lastInstFirstBlock, 0);
|
|
-
|
|
- if (block == secondBlock) {
|
|
- // perfect, the second block is the merge block then
|
|
- mergeBlock = builder.lookupResultID(reinterpret_cast<uintptr_t>(secondBlock));
|
|
- }
|
|
- } else if (lastInstOpcodeSecondBlock == LLVMBr && !DynamicLLVM::LLVMIsConditional(lastInstSecondBlock)) {
|
|
- // make sure the target block is the same
|
|
- auto block = DynamicLLVM::LLVMGetSuccessor(lastInstSecondBlock, 0);
|
|
-
|
|
- if (block == firstBlock) {
|
|
- // perfect, the first block is the merge block then
|
|
- mergeBlock = builder.lookupResultID(reinterpret_cast<uintptr_t>(firstBlock));
|
|
- }
|
|
- }
|
|
-
|
|
- if (mergeBlock != SPIRV::ResultIDInvalid) {
|
|
- cfgInfo.type = CFGType::Selection;
|
|
- cfgInfo.selectionMergeBlock = mergeBlock;
|
|
- }
|
|
- }
|
|
-
|
|
- if (cfgInfo.type != CFGType::None) {
|
|
- cfgInfos[builder.lookupResultID(reinterpret_cast<uintptr_t>(bb))] = cfgInfo;
|
|
- }
|
|
- }
|
|
-
|
|
+ std::vector<LLVMBasicBlockRef> blocks;
|
|
+ for(auto bb=DynamicLLVM::LLVMGetFirstBasicBlock(_function);bb;bb=DynamicLLVM::LLVMGetNextBasicBlock(bb))blocks.push_back(bb);
|
|
+ std::set<LLVMBasicBlockRef> all(blocks.begin(),blocks.end());
|
|
+ std::unordered_map<LLVMBasicBlockRef,std::set<LLVMBasicBlockRef>> postdom;
|
|
+ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb);postdom[bb]=DynamicLLVM::LLVMGetNumSuccessors(term)?all:std::set<LLVMBasicBlockRef>{bb};}
|
|
+ bool changed=true;
|
|
+ while(changed){changed=false;for(auto bb:blocks){
|
|
+ auto term=DynamicLLVM::LLVMGetLastInstruction(bb);auto count=DynamicLLVM::LLVMGetNumSuccessors(term);
|
|
+ if(!count)continue;
|
|
+ auto next=postdom[DynamicLLVM::LLVMGetSuccessor(term,0)];
|
|
+ for(unsigned i=1;i<count;++i){std::set<LLVMBasicBlockRef> common;auto& other=postdom[DynamicLLVM::LLVMGetSuccessor(term,i)];std::set_intersection(next.begin(),next.end(),other.begin(),other.end(),std::inserter(common,common.begin()));next=std::move(common);}
|
|
+ next.insert(bb);if(next!=postdom[bb]){postdom[bb]=std::move(next);changed=true;}
|
|
+ }}
|
|
+ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb);
|
|
+ if(!IRIDIUM_IS_BR(DynamicLLVM::LLVMGetInstructionOpcode(term)) || !DynamicLLVM::LLVMIsConditional(term))continue;
|
|
+ LLVMBasicBlockRef merge=nullptr;
|
|
+ for(auto candidate:postdom[bb])if(candidate!=bb && (!merge || postdom[candidate].size()>postdom[merge].size()))merge=candidate;
|
|
+ if(merge)cfgInfos[builder.lookupResultID(reinterpret_cast<uintptr_t>(bb))]={CFGType::Selection,builder.lookupResultID(reinterpret_cast<uintptr_t>(merge))};
|
|
+ }
|
|
+
|
|
+ // Identify natural loop back edges from dominators; their headers need OpLoopMerge.
|
|
+ std::unordered_map<LLVMBasicBlockRef,SPIRV::ResultID> splitContinues;
|
|
+ std::unordered_map<LLVMBasicBlockRef,std::pair<SPIRV::ResultID,SPIRV::ResultID>> splitLoopSelections;
|
|
+ std::unordered_map<LLVMBasicBlockRef,std::set<LLVMBasicBlockRef>> predecessors,dom;
|
|
+ for(auto bb:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(bb);for(unsigned i=0;i<DynamicLLVM::LLVMGetNumSuccessors(term);++i)predecessors[DynamicLLVM::LLVMGetSuccessor(term,i)].insert(bb);dom[bb]=bb==blocks.front()?std::set<LLVMBasicBlockRef>{bb}:all;}
|
|
+ changed=true;while(changed){changed=false;for(auto bb:blocks){if(bb==blocks.front())continue;auto next=all;for(auto pred:predecessors[bb]){std::set<LLVMBasicBlockRef> common;std::set_intersection(next.begin(),next.end(),dom[pred].begin(),dom[pred].end(),std::inserter(common,common.begin()));next=std::move(common);}next.insert(bb);if(next!=dom[bb]){dom[bb]=std::move(next);changed=true;}}}
|
|
+ for(auto latch:blocks){auto term=DynamicLLVM::LLVMGetLastInstruction(latch);for(unsigned i=0;i<DynamicLLVM::LLVMGetNumSuccessors(term);++i){
|
|
+ auto header=DynamicLLVM::LLVMGetSuccessor(term,i);if(!dom[latch].count(header))continue;
|
|
+ std::set<LLVMBasicBlockRef> loop{header,latch};std::vector<LLVMBasicBlockRef> pending;if(latch!=header)pending.push_back(latch);
|
|
+ while(!pending.empty()){auto block=pending.back();pending.pop_back();for(auto pred:predecessors[block])if(loop.insert(pred).second && pred!=header)pending.push_back(pred);}
|
|
+ LLVMBasicBlockRef merge=nullptr;for(auto candidate:postdom[header])if(!loop.count(candidate) && (!merge || postdom[candidate].size()>postdom[merge].size()))merge=candidate;
|
|
+ if(!merge)throw std::runtime_error("AIR loop has no supported merge block");
|
|
+ auto headerID=builder.lookupResultID(reinterpret_cast<uintptr_t>(header));auto latchID=builder.lookupResultID(reinterpret_cast<uintptr_t>(latch));
|
|
+ auto existing=cfgInfos.find(headerID);if(existing!=cfgInfos.end() && existing->second.type==CFGType::Loop && existing->second.continueBlock!=latchID)throw std::runtime_error("AIR loop has multiple continue blocks");
|
|
+ auto continueID=builder.reserveResultID();splitContinues[latch]=continueID;
|
|
+ cfgInfos[headerID]={CFGType::Loop,builder.lookupResultID(reinterpret_cast<uintptr_t>(merge)),continueID};
|
|
+ // Within a loop, a path that breaks leaves the enclosing construct. Ignore that
|
|
+ // path when selecting the merge for an inner selection, and stop at the continue.
|
|
+ std::unordered_map<LLVMBasicBlockRef,std::set<LLVMBasicBlockRef>> local;
|
|
+ for(auto block:loop)local[block]=block==latch?std::set<LLVMBasicBlockRef>{block}:loop;
|
|
+ bool localChanged=true;while(localChanged){localChanged=false;for(auto block:loop){if(block==latch)continue;
|
|
+ auto term=DynamicLLVM::LLVMGetLastInstruction(block);auto next=loop;
|
|
+ for(unsigned j=0;j<DynamicLLVM::LLVMGetNumSuccessors(term);++j){auto target=DynamicLLVM::LLVMGetSuccessor(term,j);if(!loop.count(target) || target==header)continue;
|
|
+ std::set<LLVMBasicBlockRef> common;std::set_intersection(next.begin(),next.end(),local[target].begin(),local[target].end(),std::inserter(common,common.begin()));next=std::move(common);}
|
|
+ next.insert(block);if(next!=local[block]){local[block]=std::move(next);localChanged=true;}
|
|
+ }}
|
|
+ auto headerTerm=DynamicLLVM::LLVMGetLastInstruction(header);
|
|
+ if(IRIDIUM_IS_BR(DynamicLLVM::LLVMGetInstructionOpcode(headerTerm)) && DynamicLLVM::LLVMIsConditional(headerTerm) && loop.count(DynamicLLVM::LLVMGetSuccessor(headerTerm,0)) && loop.count(DynamicLLVM::LLVMGetSuccessor(headerTerm,1))){
|
|
+ LLVMBasicBlockRef innerMerge=nullptr;for(auto candidate:local[header])if(candidate!=header && local[candidate]!=loop && (!innerMerge || local[candidate].size()>local[innerMerge].size()))innerMerge=candidate;
|
|
+ if(!innerMerge)throw std::runtime_error("AIR loop header selection has no merge");
|
|
+ auto selectionBlock=builder.reserveResultID();splitLoopSelections[header]={selectionBlock,builder.lookupResultID(reinterpret_cast<uintptr_t>(innerMerge))};
|
|
+ }
|
|
+ for(auto block:loop){auto id=builder.lookupResultID(reinterpret_cast<uintptr_t>(block));auto found=cfgInfos.find(id);if(found==cfgInfos.end() || found->second.type!=CFGType::Selection)continue;
|
|
+ if(block==latch){cfgInfos.erase(found);continue;}
|
|
+ LLVMBasicBlockRef innerMerge=nullptr;for(auto candidate:local[block])if(candidate!=block && candidate!=header && local[candidate]!=loop && (!innerMerge || local[candidate].size()>local[innerMerge].size()))innerMerge=candidate;
|
|
+ if(innerMerge)found->second.selectionMergeBlock=builder.lookupResultID(reinterpret_cast<uintptr_t>(innerMerge));
|
|
+ }
|
|
+
|
|
+ }}
|
|
+ // A selection that exits an enclosing construct can use an unreachable merge.
|
|
+ // Distinct constructs cannot name the same merge block in structured SPIR-V.
|
|
+ std::vector<SPIRV::ResultID> unreachableMerges;
|
|
+ std::set<SPIRV::ResultID> claimedMerges;
|
|
+ auto headers=blocks;std::sort(headers.begin(),headers.end(),[&](auto a,auto b){return dom[a].size()<dom[b].size();});
|
|
+ for(auto bb:headers){auto found=cfgInfos.find(builder.lookupResultID(reinterpret_cast<uintptr_t>(bb)));if(found==cfgInfos.end())continue;
|
|
+ auto& cfg=found->second;if(!claimedMerges.insert(cfg.selectionMergeBlock).second){auto id=builder.reserveResultID();cfg.selectionMergeBlock=id;unreachableMerges.push_back(id);}
|
|
+ }
|
|
+ // SPIR-V requires non-PHI definitions to precede their uses. AIR block order need not do so.
|
|
+ std::set<LLVMBasicBlockRef> visited;std::vector<LLVMBasicBlockRef> ordered;
|
|
+ std::function<void(LLVMBasicBlockRef)> visit=[&](auto bb){if(!visited.insert(bb).second)return;auto term=DynamicLLVM::LLVMGetLastInstruction(bb);for(unsigned i=0;i<DynamicLLVM::LLVMGetNumSuccessors(term);++i)visit(DynamicLLVM::LLVMGetSuccessor(term,i));ordered.push_back(bb);};
|
|
+ visit(blocks.front());std::reverse(ordered.begin(),ordered.end());
|
|
+ for(auto bb:blocks)if(!visited.count(bb))ordered.push_back(bb);
|
|
+ blocks=std::move(ordered);
|
|
+ // PHI outputs are visible across back edges and blocks emitted later in the AIR order.
|
|
+ for(auto bb:blocks)for(auto inst=DynamicLLVM::LLVMGetFirstInstruction(bb);inst;inst=DynamicLLVM::LLVMGetNextInstruction(inst))
|
|
+ if(DynamicLLVM::LLVMGetInstructionOpcode(inst)==LLVMPHI){auto id=builder.associateResultID(reinterpret_cast<uintptr_t>(inst));builder.setResultType(id,llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst)));}
|
|
//
|
|
// translate instructions
|
|
//
|
|
isFirst = true;
|
|
- for (LLVMBasicBlockRef bb = DynamicLLVM::LLVMGetFirstBasicBlock(_function); bb != nullptr; bb = DynamicLLVM::LLVMGetNextBasicBlock(bb)) {
|
|
+ for (auto bb : blocks) {
|
|
if (isFirst) {
|
|
isFirst = false;
|
|
|
|
@@ -1030,16 +1649,54 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
builder.insertLabel(builder.lookupResultID(reinterpret_cast<uintptr_t>(bb)));
|
|
}
|
|
|
|
+ bool blockTerminated = false;
|
|
+
|
|
for (LLVMValueRef inst = DynamicLLVM::LLVMGetFirstInstruction(bb); inst != nullptr; inst = DynamicLLVM::LLVMGetNextInstruction(inst)) {
|
|
+ translationInstruction = inst;
|
|
+ // OpKill ends its block; whatever follows it in the AIR block (its branch) is dead
|
|
+ if (blockTerminated) {
|
|
+ break;
|
|
+ }
|
|
+
|
|
auto opcode = DynamicLLVM::LLVMGetInstructionOpcode(inst);
|
|
|
|
switch (opcode) {
|
|
+ case LLVMSExt: case LLVMTrunc: case LLVMFPToUI: case LLVMFPToSI:
|
|
+ case LLVMUIToFP: case LLVMSIToFP: case LLVMFPExt: {
|
|
+ auto source=DynamicLLVM::LLVMGetOperand(inst,0);auto arg=llvmValueToResultID(builder,source);
|
|
+ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst));
|
|
+ uint16_t op=opcode==LLVMSExt?114:(opcode==LLVMTrunc?113:(opcode==LLVMFPToUI?109:(opcode==LLVMFPToSI?110:(opcode==LLVMUIToFP?112:(opcode==LLVMSIToFP?111:115)))));
|
|
+ auto resultType=opcode==LLVMFPToUI?integerTypeWithSign(builder,type,false):type;
|
|
+ if(opcode==LLVMUIToFP){auto unsignedType=integerTypeWithSign(builder,llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(source)),false);arg=builder.encodeBitcast(unsignedType,arg);}
|
|
+ auto result=builder.encodeRawOp(op,resultType,{arg});if(resultType!=type)result=builder.encodeBitcast(type,result);
|
|
+ builder.associateExistingResultID(result,reinterpret_cast<uintptr_t>(inst));builder.setResultType(result,type);
|
|
+ } break;
|
|
+ case LLVMFNeg: {
|
|
+ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst));auto result=builder.encodeRawOp(127,type,{llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,0))});
|
|
+ builder.associateExistingResultID(result,reinterpret_cast<uintptr_t>(inst));builder.setResultType(result,type);
|
|
+ } break;
|
|
+ case LLVMUnreachable:builder.encodeRawOpNoResult(255,{});blockTerminated=true;break;
|
|
case LLVMZExt: {
|
|
auto source = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
auto targetType = DynamicLLVM::LLVMTypeOf(inst);
|
|
auto type = llvmTypeToSPIRVType(builder, targetType);
|
|
|
|
- auto resID = builder.encodeUConvert(type, llvmValueToResultID(builder, source));
|
|
+ auto argument = llvmValueToResultID(builder, source);
|
|
+ // AIR i1 is a SPIR-V boolean, not an integer accepted by OpUConvert.
|
|
+ // Keep the declared destination type so later PHIs see the same signedness.
|
|
+ auto sourceType = DynamicLLVM::LLVMTypeOf(source);
|
|
+ auto constant = [&](int value) {
|
|
+ switch (DynamicLLVM::LLVMGetIntTypeWidth(targetType)) {
|
|
+ case 8: return builder.declareConstantScalar<int8_t>(value);
|
|
+ case 16: return builder.declareConstantScalar<int16_t>(value);
|
|
+ case 32: return builder.declareConstantScalar<int32_t>(value);
|
|
+ case 64: return builder.declareConstantScalar<int64_t>(value);
|
|
+ default: throw std::runtime_error("Unsupported AIR boolean extension width");
|
|
+ }
|
|
+ };
|
|
+ auto resID = DynamicLLVM::LLVMGetTypeKind(sourceType) == LLVMIntegerTypeKind && DynamicLLVM::LLVMGetIntTypeWidth(sourceType) == 1
|
|
+ ? builder.encodeRawOp(169, type, {argument, constant(1), constant(0)})
|
|
+ : builder.encodeBitcast(type, builder.encodeUConvert(type, argument));
|
|
|
|
builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
builder.setResultType(resID, type);
|
|
@@ -1048,6 +1705,18 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
// TODO: look into using physical addressing instead of logical addressing, which should simplify the translation process
|
|
// (since i *think* that's the same addressing mode used by Metal code)
|
|
|
|
+ case LLVMAlloca: {
|
|
+ auto allocated = DynamicLLVM::LLVMGetAllocatedType(inst);
|
|
+ auto count = DynamicLLVM::LLVMGetOperand(inst,0);
|
|
+ if (DynamicLLVM::LLVMGetValueKind(count) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(count) != 1)
|
|
+ throw std::runtime_error("Dynamic/multiple-element AIR allocation is unsupported");
|
|
+ LayoutFreeScope layoutFree(true);
|
|
+ auto valueType=llvmTypeToSPIRVType(builder,allocated);
|
|
+ auto pointerType=builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{},SPIRV::StorageClass::Function,valueType,8));
|
|
+ auto result=builder.addFunctionVariable(pointerType);
|
|
+ builder.associateExistingResultID(result,reinterpret_cast<uintptr_t>(inst));
|
|
+ } break;
|
|
+
|
|
case LLVMGetElementPtr: {
|
|
auto base = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
auto targetType = DynamicLLVM::LLVMTypeOf(inst);
|
|
@@ -1060,17 +1729,32 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
indices.push_back(llvmValueToResultID(builder, llindex));
|
|
}
|
|
|
|
- auto tmp = llvmTypeToSPIRVType(builder, targetType);
|
|
auto tmp2 = llvmValueToResultID(builder, base);
|
|
+ auto baseStorageClass = builder.reverseLookupType(builder.lookupResultType(tmp2))->pointerStorageClass;
|
|
+ LayoutFreeScope layoutFree(baseStorageClass == SPIRV::StorageClass::Private || baseStorageClass == SPIRV::StorageClass::Function);
|
|
+ auto tmp = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::PhysicalStorageBuffer, llvmTypeToSPIRVType(builder, gepElementType(inst)), 8));
|
|
|
|
// ensure the resulting pointer storage class is the same as the input pointer storage class
|
|
auto type = *builder.reverseLookupType(tmp);
|
|
auto origTypeID = builder.lookupResultType(tmp2);
|
|
auto origType = *builder.reverseLookupType(origTypeID);
|
|
+ auto sourceType=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMGetGEPSourceElementType(inst));
|
|
+ if(origType.targetType!=sourceType){origType.targetType=sourceType;origTypeID=builder.declareType(origType);tmp2=builder.encodeBitcast(origTypeID,tmp2);builder.setResultType(tmp2,origTypeID);}
|
|
type.pointerStorageClass = origType.pointerStorageClass;
|
|
auto resultType = builder.declareType(type);
|
|
auto origTypeTarget = *builder.reverseLookupType(origType.targetType);
|
|
|
|
+ if (origType.pointerStorageClass != SPIRV::StorageClass::PhysicalStorageBuffer) {
|
|
+ auto firstIndex = DynamicLLVM::LLVMGetOperand(inst,1);
|
|
+ if (DynamicLLVM::LLVMGetValueKind(firstIndex) != LLVMConstantIntValueKind || DynamicLLVM::LLVMConstIntGetZExtValue(firstIndex) != 0)
|
|
+ throw std::runtime_error("Logical AIR GEP requires a zero initial index");
|
|
+ indices.erase(indices.begin());
|
|
+ auto result=indices.empty() ? tmp2 : builder.encodeAccessChain(resultType,tmp2,indices);
|
|
+ builder.associateExistingResultID(result,reinterpret_cast<uintptr_t>(inst));
|
|
+ builder.setResultType(result,resultType);
|
|
+ break;
|
|
+ }
|
|
+
|
|
// strangely enough, SPIR-V provides no instruction that can do an initial pointer offset like LLVM's GEP does.
|
|
// there's OpPtrAccessChain, which is tantalizingly named, but unfortunately that instruction doesn't work as expected
|
|
// either. i've tried using the raw index (i.e. in element units) and the multiplied index (i.e. in bytes), but no dice.
|
|
@@ -1112,6 +1796,9 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
op = access;
|
|
}
|
|
|
|
+ // Opaque LLVM pointers may be used with more than one load type.
|
|
+ auto currentPointer=*builder.reverseLookupType(builder.lookupResultType(op));
|
|
+ if(currentPointer.targetType!=type){auto pointer=builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag{},currentPointer.pointerStorageClass,type,8));op=builder.encodeBitcast(pointer,op);builder.setResultType(op,pointer);}
|
|
auto resID = builder.encodeLoad(type, op, alignment);
|
|
|
|
builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
@@ -1149,50 +1836,8 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto arg = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
|
|
resID = builder.encodeConvertUToF(type, llvmValueToResultID(builder, arg));
|
|
- } else if (name == "air.sample_texture_2d.v4f16" || name == "air.sample_texture_2d.v4f32" || name == "air.sample_texture_cube.v4f16") {
|
|
- auto textureArg = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
- auto samplerArg = DynamicLLVM::LLVMGetOperand(inst, 1);
|
|
- auto textureCoordArg = DynamicLLVM::LLVMGetOperand(inst, 2);
|
|
- auto someBooleanArgTODO = DynamicLLVM::LLVMGetOperand(inst, 3);
|
|
- auto offsetArg = DynamicLLVM::LLVMGetOperand(inst, 4);
|
|
- auto someOtherBooleanArgTODO = DynamicLLVM::LLVMGetOperand(inst, 5);
|
|
- auto someFloatArgTODO = DynamicLLVM::LLVMGetOperand(inst, 6);
|
|
- auto someOtherFloatArgTODO = DynamicLLVM::LLVMGetOperand(inst, 6);
|
|
- auto someI32ArgTODO = DynamicLLVM::LLVMGetOperand(inst, 7);
|
|
-
|
|
- auto textureArgID = llvmValueToResultID(builder, textureArg);
|
|
- auto samplerArgID = llvmValueToResultID(builder, samplerArg);
|
|
-
|
|
- auto texturePtrType = builder.lookupResultType(textureArgID);
|
|
- auto textureType = builder.reverseLookupType(texturePtrType)->targetType;
|
|
- auto textureLoad = builder.encodeLoad(textureType, textureArgID);
|
|
-
|
|
- auto samplerType = builder.declareType(SPIRV::Type(SPIRV::Type::SamplerTag {}));
|
|
- auto samplerPtrType = builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, SPIRV::StorageClass::UniformConstant, samplerType, 8));
|
|
-#if 1
|
|
- auto samplerLoad = builder.encodeLoad(samplerType, samplerArgID);
|
|
-#else
|
|
- auto samplerArgCast = builder.encodeBitcast(samplerPtrType, samplerArgID);
|
|
- auto samplerLoad = builder.encodeLoad(samplerType, samplerArgCast);
|
|
-#endif
|
|
-
|
|
- auto sampledImageType = builder.declareType(SPIRV::Type(SPIRV::Type::SampledImageTag {}, textureType));
|
|
- auto sampledImage = builder.encodeSampledImage(sampledImageType, textureLoad, samplerLoad);
|
|
-
|
|
- // WORKAROUND: as explained before, we use 32-bit floats in place of 16-bit floats for image sampling.
|
|
- auto textureSampleComponentType = builder.declareType(SPIRV::Type(SPIRV::Type::FloatTag {}, 32));
|
|
- auto textureSampleType = builder.declareType(SPIRV::Type(SPIRV::Type::VectorTag {}, 4, textureSampleComponentType, 16, 8));
|
|
- auto sampled = builder.encodeImageSampleImplicitLod(textureSampleType, sampledImage, llvmValueToResultID(builder, textureCoordArg));
|
|
-
|
|
- if (name == "air.sample_texture_2d.v4f16" || name == "air.sample_texture_cube.v4f16") {
|
|
- auto convertedComponentType = builder.declareType(SPIRV::Type(SPIRV::Type::FloatTag {}, 16));
|
|
- auto convertedType = builder.declareType(SPIRV::Type(SPIRV::Type::VectorTag {}, 4, convertedComponentType, 8, 8));
|
|
- sampled = builder.encodeFConvert(convertedType, sampled);
|
|
- }
|
|
-
|
|
- auto partialResult = builder.encodeCompositeInsert(type, sampled, builder.declareUndefinedValue(type), { 0 });
|
|
- resID = builder.encodeCompositeInsert(type, builder.declareConstantScalar<int8_t>(0), partialResult, { 1 });
|
|
- } else if (name == "air.convert.f.v4f32.f.v4f16" || name == "air.convert.f.v4f16.f.v4f32") {
|
|
+ } else if (translateAirTexture(builder, inst, name, type, funcInfo.type, resID)) {
|
|
+ } else if (name == "air.convert.f.v4f32.f.v4f16" || name == "air.convert.f.v4f16.f.v4f32") {
|
|
auto arg = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
|
|
resID = builder.encodeFConvert(type, llvmValueToResultID(builder, arg));
|
|
@@ -1223,14 +1868,33 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto operand2 = DynamicLLVM::LLVMGetOperand(inst, 1);
|
|
|
|
resID = builder.encodeFMax(type, llvmValueToResultID(builder, operand1), llvmValueToResultID(builder, operand2));
|
|
+ } else if (translateAirIntrinsic(builder, inst, name, type, resID, blockTerminated)) {
|
|
+ // handled
|
|
} else {
|
|
- throw std::runtime_error(std::string("TODO: support actual function calls (name = ") + name.data() + ")");
|
|
+ throw std::runtime_error(std::string("TODO: support actual function calls (name = ") + std::string(name) + ")");
|
|
}
|
|
|
|
- builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
- builder.setResultType(resID, type);
|
|
+ if (resID != SPIRV::ResultIDInvalid) {
|
|
+ builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
+ builder.setResultType(resID, type);
|
|
+ }
|
|
} break;
|
|
|
|
+ case LLVMAdd: case LLVMSub: case LLVMMul: case LLVMUDiv: case LLVMSDiv:
|
|
+ case LLVMURem: case LLVMSRem: case LLVMShl: case LLVMLShr: case LLVMAShr:
|
|
+ case LLVMAnd: case LLVMOr: case LLVMXor: {
|
|
+ auto lhs=llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,0));
|
|
+ auto rhs=llvmValueToResultID(builder,DynamicLLVM::LLVMGetOperand(inst,1));
|
|
+ auto type=llvmTypeToSPIRVType(builder,DynamicLLVM::LLVMTypeOf(inst));
|
|
+ uint16_t op=0;
|
|
+ switch(opcode){case LLVMAdd:op=128;break;case LLVMSub:op=130;break;case LLVMMul:op=132;break;
|
|
+ case LLVMUDiv:op=134;break;case LLVMSDiv:op=135;break;case LLVMURem:op=137;break;case LLVMSRem:op=138;break;
|
|
+ case LLVMShl:op=196;break;case LLVMLShr:op=194;break;case LLVMAShr:op=195;break;
|
|
+ case LLVMAnd:op=199;break;case LLVMOr:op=197;break;case LLVMXor:op=198;break;default:break;}
|
|
+ auto t=*builder.reverseLookupType(type);
|
|
+ if(t.backingType==SPIRV::Type::BackingType::Boolean){op=opcode==LLVMAnd?167:(opcode==LLVMOr?166:165);}
|
|
+ auto result=builder.encodeRawOp(op,type,{lhs,rhs});builder.associateExistingResultID(result,reinterpret_cast<uintptr_t>(inst));
|
|
+ } break;
|
|
case LLVMFMul:
|
|
case LLVMFDiv:
|
|
case LLVMFAdd:
|
|
@@ -1365,28 +2029,81 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto op1 = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
auto op2 = DynamicLLVM::LLVMGetOperand(inst, 1);
|
|
auto predicate = DynamicLLVM::LLVMGetFCmpPredicate(inst);
|
|
+ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst));
|
|
|
|
- SPIRV::ResultID resID = SPIRV::ResultIDInvalid;
|
|
- auto type = builder.declareType(SPIRV::Type(SPIRV::Type::BooleanTag {}));
|
|
-
|
|
+ uint16_t spirvOpcode = 0;
|
|
switch (predicate) {
|
|
- case LLVMRealOGT: {
|
|
- resID = builder.encodeFOrdGreaterThan(llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2));
|
|
- } break;
|
|
+ case LLVMRealOEQ: spirvOpcode = SPIRVOp::FOrdEqual; break;
|
|
+ case LLVMRealOGT: spirvOpcode = SPIRVOp::FOrdGreaterThan; break;
|
|
+ case LLVMRealOGE: spirvOpcode = SPIRVOp::FOrdGreaterThanEqual; break;
|
|
+ case LLVMRealOLT: spirvOpcode = SPIRVOp::FOrdLessThan; break;
|
|
+ case LLVMRealOLE: spirvOpcode = SPIRVOp::FOrdLessThanEqual; break;
|
|
+ case LLVMRealONE: spirvOpcode = SPIRVOp::FOrdNotEqual; break;
|
|
+ case LLVMRealORD: spirvOpcode = SPIRVOp::Ordered; break;
|
|
+ case LLVMRealUNO: spirvOpcode = SPIRVOp::Unordered; break;
|
|
+ case LLVMRealUEQ: spirvOpcode = SPIRVOp::FUnordEqual; break;
|
|
+ case LLVMRealUGT: spirvOpcode = SPIRVOp::FUnordGreaterThan; break;
|
|
+ case LLVMRealUGE: spirvOpcode = SPIRVOp::FUnordGreaterThanEqual; break;
|
|
+ case LLVMRealULT: spirvOpcode = SPIRVOp::FUnordLessThan; break;
|
|
+ case LLVMRealULE: spirvOpcode = SPIRVOp::FUnordLessThanEqual; break;
|
|
+ case LLVMRealUNE: spirvOpcode = SPIRVOp::FUnordNotEqual; break;
|
|
+ default:
|
|
+ throw std::runtime_error("TODO: handle fcmp predicate: " + std::to_string(predicate));
|
|
+ }
|
|
+
|
|
+ auto resID = builder.encodeRawOp(spirvOpcode, type, { llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2) });
|
|
|
|
- case LLVMRealOLT: {
|
|
- resID = builder.encodeFOrdLessThan(llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2));
|
|
- } break;
|
|
+ builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
+ builder.setResultType(resID, type);
|
|
+ } break;
|
|
|
|
+ case LLVMICmp: {
|
|
+ auto op1 = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
+ auto op2 = DynamicLLVM::LLVMGetOperand(inst, 1);
|
|
+ auto predicate = DynamicLLVM::LLVMGetICmpPredicate(inst);
|
|
+ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst));
|
|
+
|
|
+ uint16_t spirvOpcode = 0;
|
|
+ switch (predicate) {
|
|
+ case LLVMIntEQ: spirvOpcode = SPIRVOp::IEqual; break;
|
|
+ case LLVMIntNE: spirvOpcode = SPIRVOp::INotEqual; break;
|
|
+ case LLVMIntUGT: spirvOpcode = SPIRVOp::UGreaterThan; break;
|
|
+ case LLVMIntUGE: spirvOpcode = SPIRVOp::UGreaterThanEqual; break;
|
|
+ case LLVMIntULT: spirvOpcode = SPIRVOp::ULessThan; break;
|
|
+ case LLVMIntULE: spirvOpcode = SPIRVOp::ULessThanEqual; break;
|
|
+ case LLVMIntSGT: spirvOpcode = SPIRVOp::SGreaterThan; break;
|
|
+ case LLVMIntSGE: spirvOpcode = SPIRVOp::SGreaterThanEqual; break;
|
|
+ case LLVMIntSLT: spirvOpcode = SPIRVOp::SLessThan; break;
|
|
+ case LLVMIntSLE: spirvOpcode = SPIRVOp::SLessThanEqual; break;
|
|
default:
|
|
- throw std::runtime_error("TODO: handle fcmp predicate: " + std::to_string(predicate));
|
|
+ throw std::runtime_error("TODO: handle icmp predicate: " + std::to_string(predicate));
|
|
}
|
|
|
|
+ auto resID = builder.encodeRawOp(spirvOpcode, type, { llvmValueToResultID(builder, op1), llvmValueToResultID(builder, op2) });
|
|
+
|
|
+ builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
+ builder.setResultType(resID, type);
|
|
+ } break;
|
|
+
|
|
+ case LLVMSelect: {
|
|
+ auto type = llvmTypeToSPIRVType(builder, DynamicLLVM::LLVMTypeOf(inst));
|
|
+ auto resID = builder.encodeRawOp(SPIRVOp::Select, type, {
|
|
+ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 0)),
|
|
+ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 1)),
|
|
+ llvmValueToResultID(builder, DynamicLLVM::LLVMGetOperand(inst, 2)),
|
|
+ });
|
|
+
|
|
builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
builder.setResultType(resID, type);
|
|
} break;
|
|
|
|
- case LLVMBr: {
|
|
+ IRIDIUM_BR_CASES: {
|
|
+ auto loop=cfgInfos.find(builder.lookupResultID(reinterpret_cast<uintptr_t>(bb)));
|
|
+ if(loop!=cfgInfos.end() && loop->second.type==CFGType::Loop){
|
|
+ builder.encodeRawOpNoResult(246,{loop->second.selectionMergeBlock,loop->second.continueBlock,0});
|
|
+ auto selection=splitLoopSelections.find(bb);if(selection!=splitLoopSelections.end()){builder.encodeBranch(selection->second.first);builder.insertLabel(selection->second.first);builder.encodeSelectionMerge(selection->second.second);}
|
|
+ }
|
|
+ auto split=splitContinues.find(bb);if(split!=splitContinues.end()){builder.encodeBranch(split->second);builder.insertLabel(split->second);}
|
|
if (DynamicLLVM::LLVMIsConditional(inst)) {
|
|
auto condition = DynamicLLVM::LLVMGetCondition(inst);
|
|
auto trueLabel = DynamicLLVM::LLVMBasicBlockAsValue(DynamicLLVM::LLVMGetSuccessor(inst, 0));
|
|
@@ -1422,25 +2139,38 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
auto type = llvmTypeToSPIRVType(builder, lltype);
|
|
|
|
std::vector<std::pair<SPIRV::ResultID, SPIRV::ResultID>> variablesAndBlocks;
|
|
+ std::vector<uintptr_t> forwardAssociations;
|
|
|
|
auto opCount = DynamicLLVM::LLVMCountIncoming(inst);
|
|
for (size_t i = 0; i < opCount; ++i) {
|
|
auto variable = DynamicLLVM::LLVMGetIncomingValue(inst, i);
|
|
auto label = DynamicLLVM::LLVMBasicBlockAsValue(DynamicLLVM::LLVMGetIncomingBlock(inst, i));
|
|
- variablesAndBlocks.push_back(std::make_pair(llvmValueToResultID(builder, variable), llvmValueToResultID(builder, label)));
|
|
+ auto id=builder.lookupResultID(reinterpret_cast<uintptr_t>(variable));
|
|
+ bool forward=id==SPIRV::ResultIDInvalid && DynamicLLVM::LLVMGetValueKind(variable)==LLVMInstructionValueKind;
|
|
+ auto split=splitContinues.find(DynamicLLVM::LLVMGetIncomingBlock(inst,i));
|
|
+ auto labelID=split==splitContinues.end()?llvmValueToResultID(builder,label):split->second;
|
|
+ auto selection=splitLoopSelections.find(DynamicLLVM::LLVMGetIncomingBlock(inst,i));if(selection!=splitLoopSelections.end())labelID=selection->second.first;
|
|
+ variablesAndBlocks.push_back({forward?SPIRV::ResultIDInvalid:llvmValueToResultID(builder,variable),labelID});
|
|
+ forwardAssociations.push_back(forward?reinterpret_cast<uintptr_t>(variable):0);
|
|
}
|
|
|
|
- auto resID = builder.encodePhi(type, variablesAndBlocks);
|
|
+ auto resID = builder.encodePhi(type, variablesAndBlocks, builder.lookupResultID(reinterpret_cast<uintptr_t>(inst)), forwardAssociations);
|
|
|
|
builder.associateExistingResultID(resID, reinterpret_cast<uintptr_t>(inst));
|
|
builder.setResultType(resID, type);
|
|
} break;
|
|
|
|
case LLVMBitCast: {
|
|
+ // Unused opaque-pointer casts can infer a different pointee type.
|
|
+ // They have no effect and logical-pointer OpBitcast is illegal in SPIR-V.
|
|
+ if (!DynamicLLVM::LLVMGetFirstUse(inst)) break;
|
|
auto arg = DynamicLLVM::LLVMGetOperand(inst, 0);
|
|
auto lltype = DynamicLLVM::LLVMTypeOf(inst);
|
|
- auto type = llvmTypeToSPIRVType(builder, lltype);
|
|
auto argID = llvmValueToResultID(builder, arg);
|
|
+ auto sourceType = *builder.reverseLookupType(builder.lookupResultType(argID));
|
|
+ auto type = DynamicLLVM::LLVMGetTypeKind(lltype) == LLVMPointerTypeKind ?
|
|
+ builder.declareType(SPIRV::Type(SPIRV::Type::PointerTag {}, sourceType.pointerStorageClass,
|
|
+ llvmTypeToSPIRVType(builder, pointerElementType(inst)), 8)) : llvmTypeToSPIRVType(builder, lltype);
|
|
|
|
// ensure the resulting pointer storage class is the same as the input pointer storage class
|
|
auto typeInst = *builder.reverseLookupType(type);
|
|
@@ -1474,6 +2204,7 @@ void Iridium::AIR::Function::analyze(SPIRV::Builder& builder, OutputInfo& output
|
|
}
|
|
}
|
|
|
|
+ for(auto label:unreachableMerges){builder.insertLabel(label);builder.encodeRawOpNoResult(255,{});}
|
|
builder.endFunction();
|
|
};
|
|
|
|
diff --git a/src/iridium/spirv.cpp b/src/iridium/spirv.cpp
|
|
index 5528342..4c085e6 100644
|
|
--- a/src/iridium/spirv.cpp
|
|
+++ b/src/iridium/spirv.cpp
|
|
@@ -1,3 +1,4 @@
|
|
+#include <algorithm>
|
|
#include <iridium/spirv.hpp>
|
|
#include <optional>
|
|
#include <functional>
|
|
@@ -19,6 +20,7 @@ bool Iridium::SPIRV::Type::operator==(const Type& other) const {
|
|
backingType != other.backingType ||
|
|
scalarWidth != other.scalarWidth ||
|
|
integerIsSigned != other.integerIsSigned ||
|
|
+ layoutFree != other.layoutFree ||
|
|
entryCount != other.entryCount ||
|
|
structureMembers.size() != other.structureMembers.size() ||
|
|
pointerStorageClass != other.pointerStorageClass ||
|
|
@@ -224,12 +226,19 @@ void Iridium::SPIRV::Builder::addEntryPoint(const EntryPoint& entryPoint) {
|
|
_entryPoints.push_back(entryPoint);
|
|
};
|
|
|
|
-Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addGlobalVariable(ResultID typeID, StorageClass storageClass) {
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addGlobalVariable(ResultID typeID, StorageClass storageClass, ResultID initializer) {
|
|
auto resultID = reserveResultID();
|
|
- _globalVariables[resultID] = GlobalVariable { storageClass, typeID };
|
|
+ _globalVariables[resultID] = GlobalVariable { storageClass, typeID, initializer };
|
|
return resultID;
|
|
};
|
|
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::addFunctionVariable(ResultID typeID) {
|
|
+ auto id = reserveResultID();
|
|
+ _currentFunctionInfo->variables[id] = FunctionVariable { typeID, ResultIDInvalid };
|
|
+ setResultType(id, typeID);
|
|
+ return id;
|
|
+};
|
|
+
|
|
void Iridium::SPIRV::Builder::addDecoration(ResultID resultID, const Decoration& decoration) {
|
|
if (_decorations.find(resultID) == _decorations.end()) {
|
|
_decorations[resultID] = std::unordered_set<Decoration> { decoration };
|
|
@@ -281,6 +290,9 @@ Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::beginFunction(ResultID id) {
|
|
};
|
|
|
|
void Iridium::SPIRV::Builder::endFunction() {
|
|
+ auto position=_currentFunctionWriter->offset();
|
|
+ for(auto fixup:_phiFixups){auto id=lookupResultID(fixup.second);if(id==ResultIDInvalid)throw std::runtime_error("Unresolved forward PHI input");_currentFunctionWriter->seek(fixup.first);_currentFunctionWriter->writeIntegerLE<uint32_t>(id);}
|
|
+ _phiFixups.clear();_currentFunctionWriter->seek(position);
|
|
_currentFunctionWriter = nullptr;
|
|
};
|
|
|
|
@@ -425,7 +437,8 @@ Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::insertLabel(ResultID id) {
|
|
};
|
|
|
|
void Iridium::SPIRV::Builder::referenceGlobalVariable(ResultID id) {
|
|
- _currentFunctionInfo->referencedGlobalVariables.push_back(id);
|
|
+ auto& refs = _currentFunctionInfo->referencedGlobalVariables;
|
|
+ if (std::find(refs.begin(), refs.end(), id) == refs.end()) refs.push_back(id);
|
|
};
|
|
|
|
Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeUConvert(ResultID typeID, ResultID operand) {
|
|
@@ -719,12 +732,15 @@ void Iridium::SPIRV::Builder::encodeBranchConditional(ResultID condition, Result
|
|
endInstruction(std::move(tmp));
|
|
};
|
|
|
|
-Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodePhi(ResultID resultTypeID, std::vector<std::pair<ResultID, ResultID>> variablesAndBlocks) {
|
|
- auto result = reserveResultID();
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodePhi(ResultID resultTypeID, std::vector<std::pair<ResultID, ResultID>> variablesAndBlocks, ResultID result, std::vector<uintptr_t> forwardAssociations) {
|
|
+ if(result==ResultIDInvalid)result = reserveResultID();
|
|
auto tmp = beginInstruction(Opcode::Phi, *_currentFunctionWriter);
|
|
_currentFunctionWriter->writeIntegerLE<uint32_t>(resultTypeID);
|
|
_currentFunctionWriter->writeIntegerLE<uint32_t>(result);
|
|
- for (const auto& [variable, block]: variablesAndBlocks) {
|
|
+ size_t index=0;
|
|
+ for (const auto& [variable, block]: variablesAndBlocks) {
|
|
+ if(index<forwardAssociations.size() && forwardAssociations[index])_phiFixups.push_back({_currentFunctionWriter->offset(),forwardAssociations[index]});
|
|
+ ++index;
|
|
_currentFunctionWriter->writeIntegerLE<uint32_t>(variable);
|
|
_currentFunctionWriter->writeIntegerLE<uint32_t>(block);
|
|
}
|
|
@@ -910,6 +926,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) {
|
|
}
|
|
endInstruction(std::move(tmp));
|
|
}
|
|
+ if (type.layoutFree) continue;
|
|
tmp = beginInstruction(Opcode::MemberDecorate, _writer);
|
|
_writer.writeIntegerLE<uint32_t>(id);
|
|
_writer.writeIntegerLE<uint32_t>(i);
|
|
@@ -934,7 +951,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) {
|
|
|
|
// now emit automatic type decorations
|
|
for (const auto& [type, id]: _typeIDs) {
|
|
- if (type.backingType == Type::BackingType::RuntimeArray || type.backingType == Type::BackingType::Array) {
|
|
+ if (!type.layoutFree && (type.backingType == Type::BackingType::RuntimeArray || type.backingType == Type::BackingType::Array)) {
|
|
auto elmTypeInst = _reverseTypeIDs[type.targetType];
|
|
tmp = beginInstruction(Opcode::Decorate, _writer);
|
|
_writer.writeIntegerLE<uint32_t>(id);
|
|
@@ -1120,7 +1137,7 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) {
|
|
_writer.writeIntegerLE<uint32_t>(var.typeID);
|
|
_writer.writeIntegerLE<uint32_t>(id);
|
|
_writer.writeIntegerLE<uint32_t>(static_cast<uint32_t>(var.storageClass));
|
|
- // TODO: support initializers
|
|
+ if (var.initializer != ResultIDInvalid) _writer.writeIntegerLE<uint32_t>(var.initializer);
|
|
endInstruction(std::move(tmp));
|
|
}
|
|
|
|
@@ -1199,3 +1216,54 @@ void* Iridium::SPIRV::Builder::finalize(size_t& outputSize) {
|
|
outputSize = _writer.size();
|
|
return _writer.extractBuffer();
|
|
};
|
|
+
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::declareConstantBool(bool value) {
|
|
+ auto typeID = declareType(Type(Type::BooleanTag {}));
|
|
+ auto key = std::make_pair(static_cast<uintmax_t>(value ? 1 : 0), typeID);
|
|
+ auto it = _constantBools.find(key);
|
|
+
|
|
+ if (it != _constantBools.end()) {
|
|
+ return it->second;
|
|
+ }
|
|
+
|
|
+ auto id = reserveResultID();
|
|
+
|
|
+ _constantBools.emplace(key, id);
|
|
+
|
|
+ auto tmp = beginInstruction(value ? Opcode::ConstantTrue : Opcode::ConstantFalse, _constants);
|
|
+ _constants.writeIntegerLE<uint32_t>(typeID);
|
|
+ _constants.writeIntegerLE<uint32_t>(id);
|
|
+ endInstruction(std::move(tmp));
|
|
+
|
|
+ return id;
|
|
+};
|
|
+
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeRawOp(uint16_t opcode, ResultID resultTypeID, const std::vector<ResultID>& operands) {
|
|
+ auto result = reserveResultID();
|
|
+ auto tmp = beginInstruction(opcode, *_currentFunctionWriter);
|
|
+ _currentFunctionWriter->writeIntegerLE<uint32_t>(resultTypeID);
|
|
+ _currentFunctionWriter->writeIntegerLE<uint32_t>(result);
|
|
+ for (auto operand: operands) {
|
|
+ _currentFunctionWriter->writeIntegerLE<uint32_t>(operand);
|
|
+ }
|
|
+ endInstruction(std::move(tmp));
|
|
+ return result;
|
|
+};
|
|
+
|
|
+void Iridium::SPIRV::Builder::encodeRawOpNoResult(uint16_t opcode, const std::vector<ResultID>& operands) {
|
|
+ auto tmp = beginInstruction(opcode, *_currentFunctionWriter);
|
|
+ for (auto operand: operands) {
|
|
+ _currentFunctionWriter->writeIntegerLE<uint32_t>(operand);
|
|
+ }
|
|
+ endInstruction(std::move(tmp));
|
|
+};
|
|
+
|
|
+Iridium::SPIRV::ResultID Iridium::SPIRV::Builder::encodeGLSLOp(GLSLOpcode opcode, ResultID resultTypeID, const std::vector<ResultID>& operands) {
|
|
+ auto result = reserveResultID();
|
|
+ auto tmp = beginGLSLInstruction(opcode, *_currentFunctionWriter, resultTypeID, result);
|
|
+ for (auto operand: operands) {
|
|
+ _currentFunctionWriter->writeIntegerLE<uint32_t>(operand);
|
|
+ }
|
|
+ endInstruction(std::move(tmp));
|
|
+ return result;
|
|
+};
|
|
diff --git a/private-include/indium/counters.private.hpp b/private-include/indium/counters.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/counters.private.hpp
|
|
@@ -0,0 +1,44 @@
|
|
+#pragma once
|
|
+
|
|
+#include <cstdio>
|
|
+#include <map>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+#include <time.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+namespace Indium {
|
|
+ // Diagnostic SPIR-V/binding dumps: checked once per process (an access() per draw is a syscall plus path translation).
|
|
+ inline bool spirvDebugEnabled() {
|
|
+ static const bool enabled = access("/tmp/metal-debug-spirv", F_OK) == 0;
|
|
+ return enabled;
|
|
+ }
|
|
+
|
|
+ // Diagnostic event counters, written to the guest's /tmp/metal-trace.log every 5 s when /tmp/metal-debug-timing exists.
|
|
+ inline void count(const char* what, double amount = 1) {
|
|
+ static const bool enabled = access("/tmp/metal-debug-timing", F_OK) == 0;
|
|
+ if (!enabled) {
|
|
+ return;
|
|
+ }
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, std::pair<unsigned long, double>> counts;
|
|
+ static time_t last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ auto& entry = counts[what];
|
|
+ entry.first++;
|
|
+ entry.second += amount;
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 5) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(log, "indium-counts@%ld:", (long) now);
|
|
+ for (auto& item: counts) {
|
|
+ fprintf(log, " %s[n=%lu sum=%.0f]", item.first.c_str(), item.second.first, item.second.second);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ counts.clear();
|
|
+ }
|
|
+ }
|
|
+};
|
|
diff --git a/private-include/indium/deferred-render-encoder.private.hpp b/private-include/indium/deferred-render-encoder.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/deferred-render-encoder.private.hpp
|
|
@@ -0,0 +1,372 @@
|
|
+#pragma once
|
|
+
|
|
+// Deferred render-command encoding.
|
|
+//
|
|
+// League's main thread spends 2-4.5 ms of every frame inside the Metal-to-Vulkan translation (PrivateRenderCommandEncoder:
|
|
+// binding bookkeeping, descriptor sets, vkCmd* calls). Nothing about that work has to happen on the game's thread: Metal
|
|
+// semantics only require that the encoder calls are applied in order before the command buffer is committed.
|
|
+//
|
|
+// This facade implements the public RenderCommandEncoder interface by recording every call into a compact op list
|
|
+// (copying anything the caller may reuse: setBytes data, viewport/scissor arrays) and, at endEncoding(), hands the list to
|
|
+// the ordered worker of submit-thread.private.hpp, which creates the real PrivateRenderCommandEncoder, replays the ops into
|
|
+// it and ends it. The same worker later runs the command buffer's commit(), so encoders are always fully replayed before
|
|
+// the commit looks at them. Everything that touches the same Vulkan command buffer from another thread (blit/compute
|
|
+// encoders, a direct render encoder) first waits for the command buffer's pending replays (waitDeferred()).
|
|
+//
|
|
+// Live switch (guest file, re-read every 256 encoders): /tmp/metal-async-encode present = on. Off by default while it is
|
|
+// validated. Header-only: the build script compiles only the .cpp files of the CMake target.
|
|
+
|
|
+#include <indium/render-command-encoder.hpp>
|
|
+#include <indium/render-command-encoder.private.hpp>
|
|
+#include <indium/command-buffer.private.hpp>
|
|
+#include <indium/render-pass.hpp>
|
|
+#include <indium/submit-thread.private.hpp>
|
|
+
|
|
+#include <atomic>
|
|
+#include <cstdint>
|
|
+#include <cstdio>
|
|
+#include <dlfcn.h>
|
|
+#include <memory>
|
|
+#include <mutex>
|
|
+#include <vector>
|
|
+#include <unistd.h>
|
|
+#include <cstring>
|
|
+#include <cstdlib>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace AsyncEncode {
|
|
+ inline bool enabled() {
|
|
+ static std::atomic<bool> value {false};
|
|
+ static std::atomic<unsigned> calls {0};
|
|
+ // On by default in the League game process only (fight A/B 2026-10-12: 60.4 -> 72.4 fps, p99 28.7 -> 22.9 ms
|
|
+ // with the frame-end sync in PrivateCommandBuffer::commit). Guest flag files, re-read every 256 calls:
|
|
+ // /tmp/metal-no-async-encode turns it off there, /tmp/metal-async-encode turns it on in any process.
|
|
+ static const bool leagueGame = [] { const char* name = getprogname(); return name && strcmp(name, "LeagueofLegends") == 0; }();
|
|
+ if ((calls.fetch_add(1, std::memory_order_relaxed) & 255) == 0)
|
|
+ value.store(access("/tmp/metal-async-encode", F_OK) == 0 ||
|
|
+ (leagueGame && access("/tmp/metal-no-async-encode", F_OK) != 0), std::memory_order_relaxed);
|
|
+ return value.load(std::memory_order_relaxed);
|
|
+ }
|
|
+
|
|
+ // Objective-C autorelease pool for the worker thread (libobjc is not a link dependency of Indium)
|
|
+ struct PoolScope {
|
|
+ void* token = nullptr;
|
|
+ void (*pop)(void*) = nullptr;
|
|
+ PoolScope() {
|
|
+ static void* (*push)(void) = reinterpret_cast<void* (*)(void)>(dlsym(RTLD_DEFAULT, "objc_autoreleasePoolPush"));
|
|
+ static void (*popFn)(void*) = reinterpret_cast<void (*)(void*)>(dlsym(RTLD_DEFAULT, "objc_autoreleasePoolPop"));
|
|
+ if (push && popFn) { token = push(); pop = popFn; }
|
|
+ }
|
|
+ ~PoolScope() { if (pop) pop(token); }
|
|
+ };
|
|
+
|
|
+ enum class Kind: uint8_t {
|
|
+ SetPipeline, FrontFacing, CullMode, DepthBias, DepthClip, Viewports, Scissors, BlendColor, DepthStencil, FillMode,
|
|
+ StencilRef, StencilRefFB, VisibilityMode,
|
|
+ DrawPrim, DrawIdx,
|
|
+ VBytes, VBuffer, VBufferOffset, VSampler, VSamplerClamp, VTexture,
|
|
+ FBytes, FBuffer, FBufferOffset, FSampler, FSamplerClamp, FTexture,
|
|
+ UseResource, UseResourceStages, MemoryBarrier,
|
|
+ };
|
|
+
|
|
+ struct Op {
|
|
+ Kind kind;
|
|
+ uint8_t pad8 = 0;
|
|
+ uint32_t e1 = 0, e2 = 0;
|
|
+ size_t n[5] = {0, 0, 0, 0, 0};
|
|
+ float f[4] = {0, 0, 0, 0};
|
|
+ std::shared_ptr<void> p;
|
|
+ explicit Op(Kind k) : kind(k) {}
|
|
+ };
|
|
+
|
|
+ // Recording buffers are recycled: growing them through the allocator on every encoder is what this is avoiding.
|
|
+ struct Buffers {
|
|
+ std::vector<Op> ops;
|
|
+ std::vector<uint8_t> bytes;
|
|
+ std::vector<Viewport> viewports;
|
|
+ std::vector<ScissorRect> scissors;
|
|
+ void clear() { ops.clear(); bytes.clear(); viewports.clear(); scissors.clear(); }
|
|
+ };
|
|
+
|
|
+ struct BufferPool {
|
|
+ std::mutex mutex;
|
|
+ std::vector<std::unique_ptr<Buffers>> free;
|
|
+ };
|
|
+
|
|
+ inline BufferPool& bufferPool() {
|
|
+ // The detached worker may return buffers during process teardown.
|
|
+ static auto* pool = new BufferPool;
|
|
+ return *pool;
|
|
+ }
|
|
+
|
|
+ inline std::unique_ptr<Buffers> acquireBuffers() {
|
|
+ auto& pool = bufferPool();
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(pool.mutex);
|
|
+ if (!pool.free.empty()) { auto b = std::move(pool.free.back()); pool.free.pop_back(); return b; }
|
|
+ }
|
|
+ auto b = std::make_unique<Buffers>();
|
|
+ b->ops.reserve(512);
|
|
+ b->bytes.reserve(8192);
|
|
+ return b;
|
|
+ }
|
|
+
|
|
+ inline void releaseBuffers(std::unique_ptr<Buffers> b) {
|
|
+ b->clear();
|
|
+ if (b->ops.capacity() > 65536 || b->bytes.capacity() > 1048576) return;
|
|
+ auto& pool = bufferPool();
|
|
+ std::lock_guard<std::mutex> lock(pool.mutex);
|
|
+ if (pool.free.size() < 16) pool.free.push_back(std::move(b));
|
|
+ }
|
|
+
|
|
+ // One render pass being recorded. The recording is handed to the worker in chunks while the caller is still
|
|
+ // encoding (see DeferredRenderCommandEncoder::add), so `real` and `failed` are only touched by the worker.
|
|
+ struct State {
|
|
+ std::shared_ptr<PrivateCommandBuffer> commandBuffer;
|
|
+ RenderPassDescriptor descriptor;
|
|
+ std::shared_ptr<PrivateRenderCommandEncoder> real;
|
|
+ bool failed = false;
|
|
+ State(std::shared_ptr<PrivateCommandBuffer> cb, const RenderPassDescriptor& d) : commandBuffer(std::move(cb)), descriptor(d) {}
|
|
+ };
|
|
+
|
|
+ // Apply one chunk of recorded calls; `last` also ends the real encoder.
|
|
+ inline void replay(State& st, const Buffers& r, bool last) {
|
|
+ PoolScope pool;
|
|
+ if (!st.real && !st.failed) {
|
|
+ try {
|
|
+ st.real = st.commandBuffer->renderCommandEncoderNow(st.descriptor);
|
|
+ } catch (const std::exception& e) {
|
|
+ st.failed = true;
|
|
+ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-encode: encoder creation failed: %s\n", e.what()); fclose(f); }
|
|
+ }
|
|
+ }
|
|
+ if (!st.real) return;
|
|
+ PrivateRenderCommandEncoder* real = st.real.get();
|
|
+ for (const Op& op : r.ops) {
|
|
+ try {
|
|
+ switch (op.kind) {
|
|
+ case Kind::SetPipeline: real->setRenderPipelineState(std::static_pointer_cast<RenderPipelineState>(op.p)); break;
|
|
+ case Kind::FrontFacing: real->setFrontFacingWinding(static_cast<Winding>(op.e1)); break;
|
|
+ case Kind::CullMode: real->setCullMode(static_cast<CullMode>(op.e1)); break;
|
|
+ case Kind::DepthBias: real->setDepthBias(op.f[0], op.f[1], op.f[2]); break;
|
|
+ case Kind::DepthClip: real->setDepthClipMode(static_cast<DepthClipMode>(op.e1)); break;
|
|
+ case Kind::Viewports: real->setViewports(r.viewports.data() + op.n[0], op.n[1]); break;
|
|
+ case Kind::Scissors: real->setScissorRects(r.scissors.data() + op.n[0], op.n[1]); break;
|
|
+ case Kind::BlendColor: real->setBlendColor(op.f[0], op.f[1], op.f[2], op.f[3]); break;
|
|
+ case Kind::DepthStencil: real->setDepthStencilState(std::static_pointer_cast<DepthStencilState>(op.p)); break;
|
|
+ case Kind::FillMode: real->setTriangleFillMode(static_cast<TriangleFillMode>(op.e1)); break;
|
|
+ case Kind::StencilRef: real->setStencilReferenceValue(op.e1); break;
|
|
+ case Kind::StencilRefFB: real->setStencilReferenceValue(op.e1, op.e2); break;
|
|
+ case Kind::VisibilityMode: real->setVisibilityResultMode(static_cast<VisibilityResultMode>(op.e1), op.n[0]); break;
|
|
+ case Kind::DrawPrim: real->drawPrimitives(static_cast<PrimitiveType>(op.e1), op.n[0], op.n[1], op.n[2], op.n[3]); break;
|
|
+ case Kind::DrawIdx:
|
|
+ real->drawIndexedPrimitives(static_cast<PrimitiveType>(op.e1), op.n[0], static_cast<IndexType>(op.e2),
|
|
+ std::static_pointer_cast<Buffer>(op.p), op.n[1], op.n[2], static_cast<int64_t>(op.n[3]), op.n[4]);
|
|
+ break;
|
|
+ case Kind::VBytes: real->setVertexBytes(r.bytes.data() + op.n[0], op.n[1], op.n[2]); break;
|
|
+ case Kind::VBuffer: real->setVertexBuffer(std::static_pointer_cast<Buffer>(op.p), op.n[0], op.n[1]); break;
|
|
+ case Kind::VBufferOffset: real->setVertexBufferOffset(op.n[0], op.n[1]); break;
|
|
+ case Kind::VSampler: real->setVertexSamplerState(std::static_pointer_cast<SamplerState>(op.p), op.n[0]); break;
|
|
+ case Kind::VSamplerClamp: real->setVertexSamplerState(std::static_pointer_cast<SamplerState>(op.p), op.f[0], op.f[1], op.n[0]); break;
|
|
+ case Kind::VTexture: real->setVertexTexture(std::static_pointer_cast<Texture>(op.p), op.n[0]); break;
|
|
+ case Kind::FBytes: real->setFragmentBytes(r.bytes.data() + op.n[0], op.n[1], op.n[2]); break;
|
|
+ case Kind::FBuffer: real->setFragmentBuffer(std::static_pointer_cast<Buffer>(op.p), op.n[0], op.n[1]); break;
|
|
+ case Kind::FBufferOffset: real->setFragmentBufferOffset(op.n[0], op.n[1]); break;
|
|
+ case Kind::FSampler: real->setFragmentSamplerState(std::static_pointer_cast<SamplerState>(op.p), op.n[0]); break;
|
|
+ case Kind::FSamplerClamp: real->setFragmentSamplerState(std::static_pointer_cast<SamplerState>(op.p), op.f[0], op.f[1], op.n[0]); break;
|
|
+ case Kind::FTexture: real->setFragmentTexture(std::static_pointer_cast<Texture>(op.p), op.n[0]); break;
|
|
+ case Kind::UseResource: real->useResource(std::static_pointer_cast<Resource>(op.p), static_cast<ResourceUsage>(op.e1)); break;
|
|
+ case Kind::UseResourceStages: real->useResource(std::static_pointer_cast<Resource>(op.p), static_cast<ResourceUsage>(op.e1), static_cast<RenderStages>(op.n[0])); break;
|
|
+ case Kind::MemoryBarrier: real->memoryBarrier(op.n[0], static_cast<RenderStages>(op.n[1]), static_cast<RenderStages>(op.n[2])); break;
|
|
+ }
|
|
+ } catch (const std::exception& e) {
|
|
+ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-encode: op %d failed: %s\n", (int) op.kind, e.what()); fclose(f); }
|
|
+ }
|
|
+ }
|
|
+ if (last) {
|
|
+ try { real->endEncoding(); } catch (...) {}
|
|
+ st.real.reset();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ class DeferredRenderCommandEncoder final: public RenderCommandEncoder {
|
|
+ // calls per chunk handed to the worker while the pass is still being recorded
|
|
+ static constexpr size_t chunkOps = 128;
|
|
+
|
|
+ std::shared_ptr<State> _state;
|
|
+ std::unique_ptr<Buffers> _rec;
|
|
+ Buffers* _r;
|
|
+ bool _begun = false;
|
|
+ bool _ended = false;
|
|
+
|
|
+ // The worker starts on a pass as soon as its first chunk is complete instead of after endEncoding: at the
|
|
+ // frame-end sync it used to have the whole last pass still to do while the caller waited (27% of the main
|
|
+ // thread in fights). A chunk owns the bytes/viewports/scissors its calls refer to, so any boundary is valid.
|
|
+ void flush(bool last) {
|
|
+ auto st = _state;
|
|
+ auto cb = st->commandBuffer;
|
|
+ if (!_begun) { _begun = true; cb->deferredBegin(); }
|
|
+ Buffers* chunk = _rec.release();
|
|
+ if (!cb->replayAllowed()) {
|
|
+ // an encoder dropped after the commit (a Metal contract violation): the Vulkan command buffer is
|
|
+ // closed, so drop the chunk instead of recording into it, and balance the deferred count
|
|
+ releaseBuffers(std::unique_ptr<Buffers>(chunk));
|
|
+ if (last) {
|
|
+ cb->deferredEnd();
|
|
+ st.reset();
|
|
+ }
|
|
+ if (!last) { _rec = acquireBuffers(); _r = _rec.get(); }
|
|
+ else _r = nullptr;
|
|
+ return;
|
|
+ }
|
|
+ SubmitThread::post([st, cb, chunk, last]() mutable {
|
|
+ std::unique_ptr<Buffers> owned(chunk);
|
|
+ replay(*st, *owned, last);
|
|
+ releaseBuffers(std::move(owned));
|
|
+ if (last) {
|
|
+ st.reset();
|
|
+ cb->deferredEnd();
|
|
+ }
|
|
+ });
|
|
+ if (!last) { _rec = acquireBuffers(); _r = _rec.get(); }
|
|
+ else _r = nullptr;
|
|
+ }
|
|
+
|
|
+ Op& add(Kind k) {
|
|
+ // before recording the next call: the previous ones are complete (their fields are filled in after add())
|
|
+ if (_r->ops.size() >= chunkOps) flush(false);
|
|
+ _r->ops.emplace_back(k);
|
|
+ return _r->ops.back();
|
|
+ }
|
|
+ // The op keeps the resource alive until the worker replays it; taking the reference
|
|
+ // straight from the caller's shared_ptr (the setters now take it by const reference)
|
|
+ // costs exactly one increment and no temporary, where the by-value form left an
|
|
+ // increment/decrement pair plus a store-to-load chain per forwarded bind.
|
|
+ template<class T>
|
|
+ void addRef(Kind k, const std::shared_ptr<T>& ref, size_t a, size_t b) { Op& o = add(k); o.p = ref; o.n[0] = a; o.n[1] = b; }
|
|
+
|
|
+ public:
|
|
+ DeferredRenderCommandEncoder(std::shared_ptr<PrivateCommandBuffer> cb, const RenderPassDescriptor& d):
|
|
+ _state(std::make_shared<State>(std::move(cb), d)), _rec(acquireBuffers()), _r(_rec.get()) {}
|
|
+
|
|
+ ~DeferredRenderCommandEncoder() override { endEncoding(); }
|
|
+
|
|
+ void endEncoding() override {
|
|
+ if (_ended) return;
|
|
+ _ended = true;
|
|
+ flush(true);
|
|
+ _state.reset();
|
|
+ }
|
|
+
|
|
+ void setRenderPipelineState(const std::shared_ptr<RenderPipelineState>& s) override { addRef(Kind::SetPipeline, s, 0, 0); }
|
|
+ void setFrontFacingWinding(Winding w) override { add(Kind::FrontFacing).e1 = static_cast<uint32_t>(w); }
|
|
+ void setCullMode(CullMode m) override { add(Kind::CullMode).e1 = static_cast<uint32_t>(m); }
|
|
+ void setDepthBias(float bias, float slope, float clamp) override { Op& o = add(Kind::DepthBias); o.f[0] = bias; o.f[1] = slope; o.f[2] = clamp; }
|
|
+ void setDepthClipMode(DepthClipMode m) override { add(Kind::DepthClip).e1 = static_cast<uint32_t>(m); }
|
|
+ void setViewport(const Viewport& v) override { setViewports(&v, 1); }
|
|
+ void setViewports(const Viewport* v, size_t count) override {
|
|
+ Op& o = add(Kind::Viewports); o.n[0] = _r->viewports.size(); o.n[1] = count;
|
|
+ _r->viewports.insert(_r->viewports.end(), v, v + count);
|
|
+ }
|
|
+ void setViewports(const std::vector<Viewport>& v) override { setViewports(v.data(), v.size()); }
|
|
+ void setScissorRect(const ScissorRect& s) override { setScissorRects(&s, 1); }
|
|
+ void setScissorRects(const ScissorRect* s, size_t count) override {
|
|
+ Op& o = add(Kind::Scissors); o.n[0] = _r->scissors.size(); o.n[1] = count;
|
|
+ _r->scissors.insert(_r->scissors.end(), s, s + count);
|
|
+ }
|
|
+ void setScissorRects(const std::vector<ScissorRect>& s) override { setScissorRects(s.data(), s.size()); }
|
|
+ void setBlendColor(float r, float g, float b, float a) override { Op& o = add(Kind::BlendColor); o.f[0] = r; o.f[1] = g; o.f[2] = b; o.f[3] = a; }
|
|
+ void setDepthStencilState(const std::shared_ptr<DepthStencilState>& s) override { addRef(Kind::DepthStencil, s, 0, 0); }
|
|
+ void setTriangleFillMode(TriangleFillMode m) override { add(Kind::FillMode).e1 = static_cast<uint32_t>(m); }
|
|
+ void setStencilReferenceValue(uint32_t v) override { add(Kind::StencilRef).e1 = v; }
|
|
+ void setStencilReferenceValue(uint32_t front, uint32_t back) override { Op& o = add(Kind::StencilRefFB); o.e1 = front; o.e2 = back; }
|
|
+ void setVisibilityResultMode(VisibilityResultMode m, size_t offset) override { Op& o = add(Kind::VisibilityMode); o.e1 = static_cast<uint32_t>(m); o.n[0] = offset; }
|
|
+
|
|
+ void drawPrimitives(PrimitiveType t, size_t start, size_t count, size_t instances, size_t baseInstance) override {
|
|
+ Op& o = add(Kind::DrawPrim); o.e1 = static_cast<uint32_t>(t); o.n[0] = start; o.n[1] = count; o.n[2] = instances; o.n[3] = baseInstance;
|
|
+ }
|
|
+ void drawPrimitives(PrimitiveType t, size_t start, size_t count, size_t instances) override { drawPrimitives(t, start, count, instances, 0); }
|
|
+ void drawPrimitives(PrimitiveType t, size_t start, size_t count) override { drawPrimitives(t, start, count, 1, 0); }
|
|
+
|
|
+ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, const std::shared_ptr<Buffer>& indexBuffer, size_t indexBufferOffset,
|
|
+ size_t instanceCount, int64_t baseVertex, size_t baseInstance) override {
|
|
+ Op& o = add(Kind::DrawIdx);
|
|
+ o.e1 = static_cast<uint32_t>(t); o.e2 = static_cast<uint32_t>(it); o.p = indexBuffer;
|
|
+ o.n[0] = indexCount; o.n[1] = indexBufferOffset; o.n[2] = instanceCount; o.n[3] = static_cast<size_t>(baseVertex); o.n[4] = baseInstance;
|
|
+ }
|
|
+ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, const std::shared_ptr<Buffer>& b, size_t off, size_t instances) override {
|
|
+ drawIndexedPrimitives(t, indexCount, it, b, off, instances, 0, 0);
|
|
+ }
|
|
+ void drawIndexedPrimitives(PrimitiveType t, size_t indexCount, IndexType it, const std::shared_ptr<Buffer>& b, size_t off) override {
|
|
+ drawIndexedPrimitives(t, indexCount, it, b, off, 1, 0, 0);
|
|
+ }
|
|
+
|
|
+ void setVertexBytes(const void* bytes, size_t length, size_t index) override {
|
|
+ Op& o = add(Kind::VBytes); o.n[0] = _r->bytes.size(); o.n[1] = length; o.n[2] = index;
|
|
+ const auto* p = static_cast<const uint8_t*>(bytes);
|
|
+ _r->bytes.insert(_r->bytes.end(), p, p + length);
|
|
+ }
|
|
+ void setVertexBuffer(const std::shared_ptr<Buffer>& b, size_t offset, size_t index) override { addRef(Kind::VBuffer, b, offset, index); }
|
|
+ void setVertexBuffers(const std::vector<std::shared_ptr<Buffer>>& b, const std::vector<size_t>& o, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) addRef(Kind::VBuffer, b[i], o[i], r.start + i);
|
|
+ }
|
|
+ void setVertexBufferOffset(size_t offset, size_t index) override { Op& o = add(Kind::VBufferOffset); o.n[0] = offset; o.n[1] = index; }
|
|
+ void setVertexSamplerState(const std::shared_ptr<SamplerState>& s, size_t index) override { addRef(Kind::VSampler, s, index, 0); }
|
|
+ void setVertexSamplerState(const std::shared_ptr<SamplerState>& s, float lo, float hi, size_t index) override {
|
|
+ Op& o = add(Kind::VSamplerClamp); o.p = s; o.f[0] = lo; o.f[1] = hi; o.n[0] = index;
|
|
+ }
|
|
+ void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& s, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) setVertexSamplerState(s[i], r.start + i);
|
|
+ }
|
|
+ void setVertexSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& s, const std::vector<float>& lo, const std::vector<float>& hi, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) setVertexSamplerState(s[i], lo[i], hi[i], r.start + i);
|
|
+ }
|
|
+ void setVertexTexture(const std::shared_ptr<Texture>& t, size_t index) override { addRef(Kind::VTexture, t, index, 0); }
|
|
+ void setVertexTextures(const std::vector<std::shared_ptr<Texture>>& t, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) addRef(Kind::VTexture, t[i], r.start + i, 0);
|
|
+ }
|
|
+
|
|
+ void setFragmentBytes(const void* bytes, size_t length, size_t index) override {
|
|
+ Op& o = add(Kind::FBytes); o.n[0] = _r->bytes.size(); o.n[1] = length; o.n[2] = index;
|
|
+ const auto* p = static_cast<const uint8_t*>(bytes);
|
|
+ _r->bytes.insert(_r->bytes.end(), p, p + length);
|
|
+ }
|
|
+ void setFragmentBuffer(const std::shared_ptr<Buffer>& b, size_t offset, size_t index) override { addRef(Kind::FBuffer, b, offset, index); }
|
|
+ void setFragmentBuffers(const std::vector<std::shared_ptr<Buffer>>& b, const std::vector<size_t>& o, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) addRef(Kind::FBuffer, b[i], o[i], r.start + i);
|
|
+ }
|
|
+ void setFragmentBufferOffset(size_t offset, size_t index) override { Op& o = add(Kind::FBufferOffset); o.n[0] = offset; o.n[1] = index; }
|
|
+ void setFragmentSamplerState(const std::shared_ptr<SamplerState>& s, size_t index) override { addRef(Kind::FSampler, s, index, 0); }
|
|
+ void setFragmentSamplerState(const std::shared_ptr<SamplerState>& s, float lo, float hi, size_t index) override {
|
|
+ Op& o = add(Kind::FSamplerClamp); o.p = s; o.f[0] = lo; o.f[1] = hi; o.n[0] = index;
|
|
+ }
|
|
+ void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& s, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) setFragmentSamplerState(s[i], r.start + i);
|
|
+ }
|
|
+ void setFragmentSamplerStates(const std::vector<std::shared_ptr<SamplerState>>& s, const std::vector<float>& lo, const std::vector<float>& hi, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) setFragmentSamplerState(s[i], lo[i], hi[i], r.start + i);
|
|
+ }
|
|
+ void setFragmentTexture(const std::shared_ptr<Texture>& t, size_t index) override { addRef(Kind::FTexture, t, index, 0); }
|
|
+ void setFragmentTextures(std::vector<std::shared_ptr<Texture>>& t, Range<size_t> r) override {
|
|
+ for (size_t i = 0; i < r.length; ++i) addRef(Kind::FTexture, t[i], r.start + i, 0);
|
|
+ }
|
|
+
|
|
+ void useResource(std::shared_ptr<Resource> res, ResourceUsage usage, RenderStages stages) override {
|
|
+ Op& o = add(Kind::UseResourceStages); o.p = std::move(res); o.e1 = static_cast<uint32_t>(usage); o.n[0] = static_cast<size_t>(stages);
|
|
+ }
|
|
+ void useResources(const std::vector<std::shared_ptr<Resource>>& res, ResourceUsage usage, RenderStages stages) override {
|
|
+ // one op per resource: the real encoder batches barriers per call, which only affects how they are grouped
|
|
+ for (const auto& r : res) useResource(r, usage, stages);
|
|
+ }
|
|
+ void useResource(std::shared_ptr<Resource> res, ResourceUsage usage) override {
|
|
+ Op& o = add(Kind::UseResource); o.p = std::move(res); o.e1 = static_cast<uint32_t>(usage);
|
|
+ }
|
|
+ void useResources(const std::vector<std::shared_ptr<Resource>>& res, ResourceUsage usage) override {
|
|
+ for (const auto& r : res) useResource(r, usage);
|
|
+ }
|
|
+ void memoryBarrier(size_t scope, RenderStages after, RenderStages before) override {
|
|
+ Op& o = add(Kind::MemoryBarrier); o.n[0] = scope; o.n[1] = static_cast<size_t>(after); o.n[2] = static_cast<size_t>(before);
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+}
|
|
diff --git a/private-include/indium/gpu-timing.private.hpp b/private-include/indium/gpu-timing.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/gpu-timing.private.hpp
|
|
@@ -0,0 +1,259 @@
|
|
+#pragma once
|
|
+
|
|
+// Diagnostic GPU profiler built on Vulkan timestamp queries (guest flag files, checked once at start):
|
|
+// /tmp/metal-gpu-timing per render pass: start/end on the GPU timeline -> /tmp/indium-gpu-timing.log
|
|
+// line per command buffer: "cb <WxH>:<draws>:<ms>:<start_ns> ..."
|
|
+// /tmp/metal-gpu-timing-draws additionally a timestamp after every draw; GPU time is attributed to the pipeline
|
|
+// state (vertex+fragment function names in /tmp/indium-gpu-pso.log) and summed per
|
|
+// (pso, target size) into /tmp/indium-gpu-draws.log (rewritten every 5 s)
|
|
+// Header-only, function-local statics: no class gains a member (QuartzCore/Metal read Indium objects at fixed offsets).
|
|
+
|
|
+#include <indium/dynamic-vk.hpp>
|
|
+
|
|
+#include <chrono>
|
|
+#include <cstdint>
|
|
+#include <cstdio>
|
|
+#include <map>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+#include <tuple>
|
|
+#include <unordered_map>
|
|
+#include <vector>
|
|
+#include <unistd.h>
|
|
+#include <sys/stat.h>
|
|
+#include <sys/types.h>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace GpuTiming {
|
|
+ struct Pass {
|
|
+ uint32_t first;
|
|
+ uint32_t width;
|
|
+ uint32_t height;
|
|
+ uint32_t draws;
|
|
+ std::vector<uint64_t> drawPso; // pso id per draw timestamp, in order
|
|
+ };
|
|
+
|
|
+ struct State {
|
|
+ static constexpr uint32_t Capacity = 1u << 20; // 8 MB: ~1700 passes of in-flight history, no wrap hazard
|
|
+ static constexpr uint32_t DrawSlots = 600;
|
|
+ std::mutex mutex;
|
|
+ VkQueryPool pool = VK_NULL_HANDLE;
|
|
+ uint32_t next = 0;
|
|
+ std::unordered_map<VkCommandBuffer, std::vector<Pass>> passes;
|
|
+ std::map<std::tuple<uint64_t, uint32_t, uint32_t>, std::pair<double, uint64_t>> perPso; // ms, draws
|
|
+ std::map<std::tuple<uint64_t, uint32_t, uint32_t>, std::pair<double, uint64_t>> perPsoSingle; // passes with one draw
|
|
+ std::chrono::steady_clock::time_point lastDump = std::chrono::steady_clock::now();
|
|
+ FILE* log = nullptr;
|
|
+ bool enabled = access("/tmp/metal-gpu-timing", F_OK) == 0;
|
|
+ bool drawLevel = access("/tmp/metal-gpu-timing-draws", F_OK) == 0;
|
|
+ };
|
|
+
|
|
+ inline State& state() {
|
|
+ static State* instance = new State;
|
|
+ return *instance;
|
|
+ }
|
|
+
|
|
+ inline bool enabled() { return state().enabled; }
|
|
+
|
|
+ inline thread_local uint32_t currentDraws = 0;
|
|
+
|
|
+ inline void countDraw() {
|
|
+ if (state().enabled) ++currentDraws;
|
|
+ }
|
|
+
|
|
+ // SPIR-V library identity: FNV-1a over the module, and (draw-level mode) the module saved as <hash>.spv
|
|
+ inline std::unordered_map<const void*, std::string>& libraryIds() {
|
|
+ static auto* ids = new std::unordered_map<const void*, std::string>;
|
|
+ return *ids;
|
|
+ }
|
|
+
|
|
+ inline void noteLibrary(const void* library, const char* data, size_t length) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled || !s.drawLevel) return;
|
|
+ uint64_t h = 1469598103934665603ull;
|
|
+ for (size_t i = 0; i < length; ++i) { h ^= (unsigned char) data[i]; h *= 1099511628211ull; }
|
|
+ char id[48];
|
|
+ snprintf(id, sizeof(id), "%08llx:%zu", (unsigned long long) (h & 0xffffffffull), length);
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ libraryIds()[library] = id;
|
|
+ }
|
|
+ char path[96];
|
|
+ snprintf(path, sizeof(path), "/tmp/indium-shaders/%08llx.spv", (unsigned long long) (h & 0xffffffffull));
|
|
+ mkdir("/tmp/indium-shaders", 0755);
|
|
+ if (access(path, F_OK) != 0) {
|
|
+ if (FILE* f = fopen(path, "wb")) { fwrite(data, 1, length, f); fclose(f); }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ inline std::string libraryId(const void* library) {
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = libraryIds().find(library);
|
|
+ return it == libraryIds().end() ? std::string("-") : it->second;
|
|
+ }
|
|
+
|
|
+ inline void notePipeline(const void* pso, const std::string& vertex, const std::string& fragment) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled || !s.drawLevel) return;
|
|
+ if (FILE* f = fopen("/tmp/indium-gpu-pso.log", "a")) {
|
|
+ fprintf(f, "%llx %s %s\n", (unsigned long long) (uintptr_t) pso, vertex.c_str(), fragment.c_str());
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // before vkCmdBeginRenderPass (outside any render pass)
|
|
+ inline void beginPass(VkDevice device, VkCommandBuffer cb, uint32_t width, uint32_t height) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled) return;
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ if (s.pool == VK_NULL_HANDLE) {
|
|
+ VkQueryPoolCreateInfo info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO;
|
|
+ info.queryType = VK_QUERY_TYPE_TIMESTAMP;
|
|
+ info.queryCount = State::Capacity;
|
|
+ if (DynamicVK::vkCreateQueryPool(device, &info, nullptr, &s.pool) != VK_SUCCESS) {
|
|
+ s.enabled = false;
|
|
+ return;
|
|
+ }
|
|
+ }
|
|
+ uint32_t block = 2 + (s.drawLevel ? State::DrawSlots : 0);
|
|
+ if (s.next + block > State::Capacity) s.next = 0;
|
|
+ uint32_t first = s.next;
|
|
+ s.next += block;
|
|
+ DynamicVK::vkCmdResetQueryPool(cb, s.pool, first, block);
|
|
+ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, s.pool, first);
|
|
+ s.passes[cb].push_back(Pass { first, width, height, 0, {} });
|
|
+ currentDraws = 0;
|
|
+ }
|
|
+
|
|
+ // render target configuration of every pass: "<WxH> c<n>=<format>/<load>/<store>... d=<format>/<load>/<store>" -> count,
|
|
+ // rewritten to /tmp/indium-gpu-targets.log every 5 s (pixel format/action numbers are Indium enum values)
|
|
+ inline void noteTarget(const std::string& key) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled) return;
|
|
+ static auto* counts = new std::map<std::string, uint64_t>;
|
|
+ static auto last = std::chrono::steady_clock::now();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ ++(*counts)[key];
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
+ if (now - last < std::chrono::seconds(5)) return;
|
|
+ last = now;
|
|
+ if (FILE* f = fopen("/tmp/indium-gpu-targets.log", "w")) {
|
|
+ for (const auto& [k, v] : *counts) fprintf(f, "%llu %s\n", (unsigned long long) v, k.c_str());
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // whole command buffer: logged like a pass with size 3x3 (first pass of the command buffer, end written at commit)
|
|
+ inline void beginSpan(VkDevice device, VkCommandBuffer cb) { beginPass(device, cb, 3, 3); }
|
|
+
|
|
+ inline void endSpan(VkCommandBuffer cb) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled) return;
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.passes.find(cb);
|
|
+ if (it == s.passes.end() || it->second.empty() || it->second.front().width != 3) return;
|
|
+ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, it->second.front().first + 1);
|
|
+ }
|
|
+
|
|
+ // after each vkCmdDraw / vkCmdDrawIndexed
|
|
+ inline void afterDraw(VkCommandBuffer cb, uint64_t pso) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled || !s.drawLevel) return;
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.passes.find(cb);
|
|
+ if (it == s.passes.end() || it->second.empty()) return;
|
|
+ auto& pass = it->second.back();
|
|
+ if (pass.drawPso.size() >= State::DrawSlots - 1) return;
|
|
+ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, pass.first + 2 + (uint32_t) pass.drawPso.size());
|
|
+ pass.drawPso.push_back(pso);
|
|
+ }
|
|
+
|
|
+ // after vkCmdEndRenderPass
|
|
+ inline void endPass(VkCommandBuffer cb) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled) return;
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.passes.find(cb);
|
|
+ if (it == s.passes.end() || it->second.empty()) return;
|
|
+ auto& pass = it->second.back();
|
|
+ pass.draws = currentDraws;
|
|
+ DynamicVK::vkCmdWriteTimestamp(cb, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, s.pool, pass.first + 1);
|
|
+ }
|
|
+
|
|
+ inline void dumpDraws(State& s) {
|
|
+ if (!s.drawLevel) return;
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
+ if (now - s.lastDump < std::chrono::seconds(5)) return;
|
|
+ s.lastDump = now;
|
|
+ if (FILE* f = fopen("/tmp/indium-gpu-draws.log", "w")) {
|
|
+ for (const auto& [key, v] : s.perPso)
|
|
+ fprintf(f, "%llx %ux%u %llu %.4f\n", (unsigned long long) std::get<0>(key), std::get<1>(key), std::get<2>(key),
|
|
+ (unsigned long long) v.second, v.first);
|
|
+ fclose(f);
|
|
+ }
|
|
+ if (FILE* f = fopen("/tmp/indium-gpu-single.log", "w")) {
|
|
+ for (const auto& [key, v] : s.perPsoSingle)
|
|
+ fprintf(f, "%llx %ux%u %llu %.4f\n", (unsigned long long) std::get<0>(key), std::get<1>(key), std::get<2>(key),
|
|
+ (unsigned long long) v.second, v.first);
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // when the command buffer has completed on the GPU
|
|
+ inline void collect(VkDevice device, VkCommandBuffer cb, double periodNs) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled) return;
|
|
+ std::vector<Pass> passes;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.passes.find(cb);
|
|
+ if (it == s.passes.end()) return;
|
|
+ passes = std::move(it->second);
|
|
+ s.passes.erase(it);
|
|
+ }
|
|
+ if (passes.empty()) return;
|
|
+ std::string line;
|
|
+ char buf[96];
|
|
+ std::vector<uint64_t> t;
|
|
+ std::vector<std::tuple<uint64_t, uint32_t, uint32_t, double>> samples; // pso, w, h, ms
|
|
+ std::vector<std::tuple<uint64_t, uint32_t, uint32_t, double>> singles;
|
|
+ for (const auto& pass : passes) {
|
|
+ uint32_t n = 2 + (uint32_t) pass.drawPso.size();
|
|
+ t.assign(n, 0);
|
|
+ // the mutex is NOT held while waiting for the GPU: the render thread takes it for every pass and draw
|
|
+ if (DynamicVK::vkGetQueryPoolResults(device, s.pool, pass.first, n, n * sizeof(uint64_t), t.data(), sizeof(uint64_t),
|
|
+ VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT) != VK_SUCCESS) continue;
|
|
+ snprintf(buf, sizeof(buf), " %ux%u:%u:%.3f:%.0f", pass.width, pass.height, pass.draws,
|
|
+ (double) (t[1] - t[0]) * periodNs / 1e6, (double) t[0] * periodNs);
|
|
+ line += buf;
|
|
+ uint64_t previous = t[0];
|
|
+ for (size_t i = 0; i < pass.drawPso.size(); ++i) {
|
|
+ uint64_t now = t[2 + i];
|
|
+ double ms = now > previous ? (double) (now - previous) * periodNs / 1e6 : 0;
|
|
+ previous = now > previous ? now : previous;
|
|
+ samples.emplace_back(pass.drawPso[i], pass.width, pass.height, ms);
|
|
+ if (pass.drawPso.size() == 1) singles.emplace_back(pass.drawPso[i], pass.width, pass.height, (double) (t[1] - t[0]) * periodNs / 1e6);
|
|
+ }
|
|
+ }
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ for (const auto& [pso, w, h, ms] : samples) {
|
|
+ auto& entry = s.perPso[std::make_tuple(pso, w, h)];
|
|
+ entry.first += ms;
|
|
+ entry.second += 1;
|
|
+ }
|
|
+ for (const auto& [pso, w, h, ms] : singles) {
|
|
+ auto& entry = s.perPsoSingle[std::make_tuple(pso, w, h)];
|
|
+ entry.first += ms;
|
|
+ entry.second += 1;
|
|
+ }
|
|
+ if (!s.log) s.log = fopen("/tmp/indium-gpu-timing.log", "a");
|
|
+ if (s.log) {
|
|
+ fprintf(s.log, "cb%s\n", line.c_str());
|
|
+ fflush(s.log);
|
|
+ }
|
|
+ dumpDraws(s);
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/private-include/indium/memory-pool.private.hpp b/private-include/indium/memory-pool.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/memory-pool.private.hpp
|
|
@@ -0,0 +1,233 @@
|
|
+#pragma once
|
|
+
|
|
+// Suballocation of device memory for buffers and textures.
|
|
+//
|
|
+// Every buffer and texture used to own a VkDeviceMemory. RADV hands the kernel the complete list of a device's memory
|
|
+// objects with every submission (required once buffer device addresses are in use), so vkQueueSubmit2 cost grew with the
|
|
+// number of live resources: about 1.1 ms per frame in League, 77% of it in amdgpu_cs_ioctl walking that list.
|
|
+// Resources up to MaxPooledSize now share 32 MB blocks; larger ones keep a dedicated allocation.
|
|
+//
|
|
+// Header-only on purpose (function-local statics, one instance per libindium) and keyed by the Vulkan handle of the
|
|
+// owning buffer/image, so no class gains a member: QuartzCore and Metal read some Indium objects at fixed offsets.
|
|
+// Create guest /tmp/metal-no-pool before the process starts to get one allocation per resource again.
|
|
+
|
|
+#include <indium/dynamic-vk.hpp>
|
|
+
|
|
+#include <chrono>
|
|
+#include <cstdint>
|
|
+#include <cstdlib>
|
|
+#include <deque>
|
|
+#include <map>
|
|
+#include <memory>
|
|
+#include <mutex>
|
|
+#include <unordered_map>
|
|
+#include <vector>
|
|
+#include <unistd.h>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace MemoryPool {
|
|
+ struct Allocation {
|
|
+ VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
+ VkDeviceSize offset = 0;
|
|
+ VkDeviceSize size = 0;
|
|
+ };
|
|
+
|
|
+ namespace Detail {
|
|
+ static constexpr VkDeviceSize BlockSize = 32ull << 20;
|
|
+ static constexpr VkDeviceSize MaxPooledSize = 4ull << 20;
|
|
+ static constexpr VkDeviceSize Granule = 256;
|
|
+ // a freed range is not handed out again before this long: work already submitted to the GPU may still read it
|
|
+ /* darling-lol perf: the old 1000ms quarantine caused pool exhaustion
|
|
+ * and fallback OS allocations during high-churn gameplay. 50ms (~3
|
|
+ * frames at 60Hz) is sufficient for in-flight GPU work to drain. */
|
|
+ static constexpr std::chrono::milliseconds Quarantine {50};
|
|
+
|
|
+ struct Block {
|
|
+ VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
+ VkDeviceSize size = 0;
|
|
+ uint32_t key = 0;
|
|
+ bool dedicated = false;
|
|
+ void* mapped = nullptr;
|
|
+ size_t live = 0;
|
|
+ std::map<VkDeviceSize, VkDeviceSize> free; // offset -> size
|
|
+ };
|
|
+
|
|
+ struct Entry {
|
|
+ std::shared_ptr<Block> block;
|
|
+ VkDeviceSize offset;
|
|
+ VkDeviceSize size;
|
|
+ };
|
|
+
|
|
+ struct Pending {
|
|
+ std::chrono::steady_clock::time_point when;
|
|
+ std::shared_ptr<Block> block;
|
|
+ VkDeviceSize offset;
|
|
+ VkDeviceSize size;
|
|
+ };
|
|
+
|
|
+ struct State {
|
|
+ std::mutex mutex;
|
|
+ std::unordered_map<uint64_t, Entry> entries;
|
|
+ std::unordered_map<uint32_t, std::vector<std::shared_ptr<Block>>> blocks;
|
|
+ std::deque<Pending> pending;
|
|
+ bool disabled = access("/tmp/metal-no-pool", F_OK) == 0;
|
|
+ };
|
|
+
|
|
+ inline State& state() {
|
|
+ static State* instance = new State; // never destroyed: resources are released during process teardown
|
|
+ return *instance;
|
|
+ }
|
|
+
|
|
+ inline VkDeviceSize alignUp(VkDeviceSize value, VkDeviceSize alignment) {
|
|
+ return (value + alignment - 1) / alignment * alignment;
|
|
+ }
|
|
+
|
|
+ inline VkDeviceMemory allocateMemory(VkDevice device, uint32_t typeIndex, VkDeviceSize size, bool deviceAddress) {
|
|
+ VkMemoryAllocateInfo info {};
|
|
+ info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
|
|
+ info.allocationSize = size;
|
|
+ info.memoryTypeIndex = typeIndex;
|
|
+ VkMemoryAllocateFlagsInfo flags {};
|
|
+ flags.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_FLAGS_INFO;
|
|
+ flags.flags = VK_MEMORY_ALLOCATE_DEVICE_ADDRESS_BIT;
|
|
+ if (deviceAddress) info.pNext = &flags;
|
|
+ VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
+ if (DynamicVK::vkAllocateMemory(device, &info, nullptr, &memory) != VK_SUCCESS) abort();
|
|
+ return memory;
|
|
+ }
|
|
+
|
|
+ inline void releaseRange(State& s, VkDevice device, const std::shared_ptr<Block>& block, VkDeviceSize offset, VkDeviceSize size) {
|
|
+ auto next = block->free.lower_bound(offset);
|
|
+ if (next != block->free.begin()) {
|
|
+ auto previous = std::prev(next);
|
|
+ if (previous->first + previous->second == offset) {
|
|
+ offset = previous->first;
|
|
+ size += previous->second;
|
|
+ block->free.erase(previous);
|
|
+ }
|
|
+ }
|
|
+ if (next != block->free.end() && offset + size == next->first) {
|
|
+ size += next->second;
|
|
+ block->free.erase(next);
|
|
+ }
|
|
+ block->free[offset] = size;
|
|
+ if (block->live != 0 || size != block->size) return;
|
|
+ // the block is empty: keep one spare per kind, give the others back
|
|
+ auto& list = s.blocks[block->key];
|
|
+ size_t empty = 0;
|
|
+ for (const auto& other : list) {
|
|
+ if (other->live == 0 && other->free.size() == 1 && other->free.begin()->second == other->size) ++empty;
|
|
+ }
|
|
+ if (empty <= 1) return;
|
|
+ for (auto it = list.begin(); it != list.end(); ++it) {
|
|
+ if (*it == block) { list.erase(it); break; }
|
|
+ }
|
|
+ DynamicVK::vkFreeMemory(device, block->memory, nullptr);
|
|
+ block->memory = VK_NULL_HANDLE;
|
|
+ }
|
|
+
|
|
+ inline void drain(State& s, VkDevice device) {
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
+ while (!s.pending.empty() && now - s.pending.front().when >= Quarantine) {
|
|
+ Pending item = std::move(s.pending.front());
|
|
+ s.pending.pop_front();
|
|
+ if (item.block->dedicated) {
|
|
+ DynamicVK::vkFreeMemory(device, item.block->memory, nullptr);
|
|
+ } else {
|
|
+ --item.block->live;
|
|
+ releaseRange(s, device, item.block, item.offset, item.size);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // `owner` is the buffer or image handle the memory will be bound to
|
|
+ inline Allocation allocate(VkDevice device, uint64_t owner, uint32_t typeIndex, VkDeviceSize size, VkDeviceSize alignment, bool image, bool deviceAddress) {
|
|
+ using namespace Detail;
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ drain(s, device);
|
|
+ Entry entry;
|
|
+ if (s.disabled || size > MaxPooledSize) {
|
|
+ entry.block = std::make_shared<Block>();
|
|
+ entry.block->dedicated = true;
|
|
+ entry.block->size = size;
|
|
+ entry.block->memory = allocateMemory(device, typeIndex, size, deviceAddress);
|
|
+ entry.offset = 0;
|
|
+ entry.size = size;
|
|
+ } else {
|
|
+ if (alignment < Granule) alignment = Granule;
|
|
+ size = alignUp(size, Granule);
|
|
+ const uint32_t key = (typeIndex << 2) | (image ? 2u : 0u) | (deviceAddress ? 1u : 0u);
|
|
+ auto& list = s.blocks[key];
|
|
+ for (int attempt = 0; attempt < 2 && !entry.block; ++attempt) {
|
|
+ for (auto& block : list) {
|
|
+ for (auto it = block->free.begin(); it != block->free.end(); ++it) {
|
|
+ VkDeviceSize start = alignUp(it->first, alignment);
|
|
+ VkDeviceSize end = it->first + it->second;
|
|
+ if (start + size > end) continue;
|
|
+ VkDeviceSize rangeStart = it->first;
|
|
+ block->free.erase(it);
|
|
+ if (start > rangeStart) block->free[rangeStart] = start - rangeStart;
|
|
+ if (start + size < end) block->free[start + size] = end - (start + size);
|
|
+ entry.block = block;
|
|
+ entry.offset = start;
|
|
+ entry.size = size;
|
|
+ ++block->live;
|
|
+ break;
|
|
+ }
|
|
+ if (entry.block) break;
|
|
+ }
|
|
+ if (!entry.block && attempt == 0) {
|
|
+ auto block = std::make_shared<Block>();
|
|
+ block->key = key;
|
|
+ block->size = BlockSize;
|
|
+ block->memory = allocateMemory(device, typeIndex, BlockSize, deviceAddress);
|
|
+ block->free[0] = BlockSize;
|
|
+ list.push_back(block);
|
|
+ }
|
|
+ }
|
|
+ if (!entry.block) abort();
|
|
+ }
|
|
+ s.entries[owner] = entry;
|
|
+ return Allocation { entry.block->memory, entry.offset, entry.size };
|
|
+ }
|
|
+
|
|
+ inline bool lookup(uint64_t owner, Allocation& result) {
|
|
+ auto& s = Detail::state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.entries.find(owner);
|
|
+ if (it == s.entries.end()) return false;
|
|
+ result = Allocation { it->second.block->memory, it->second.offset, it->second.size };
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ // host pointer to the start of the owner's memory (the memory type must be host-visible)
|
|
+ inline void* map(VkDevice device, uint64_t owner) {
|
|
+ auto& s = Detail::state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.entries.find(owner);
|
|
+ if (it == s.entries.end()) return nullptr;
|
|
+ auto& block = it->second.block;
|
|
+ if (!block->mapped) {
|
|
+ if (DynamicVK::vkMapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &block->mapped) != VK_SUCCESS) abort();
|
|
+ }
|
|
+ return static_cast<char*>(block->mapped) + it->second.offset;
|
|
+ }
|
|
+
|
|
+ // call after the owning buffer/image has been destroyed
|
|
+ inline void free(VkDevice device, uint64_t owner) {
|
|
+ using namespace Detail;
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.entries.find(owner);
|
|
+ if (it == s.entries.end()) return;
|
|
+ Entry entry = std::move(it->second);
|
|
+ s.entries.erase(it);
|
|
+ if (entry.block->dedicated) {
|
|
+ // nothing can reuse a dedicated allocation, and the kernel keeps it alive for work in flight
|
|
+ DynamicVK::vkFreeMemory(device, entry.block->memory, nullptr);
|
|
+ } else {
|
|
+ s.pending.push_back(Pending { std::chrono::steady_clock::now(), entry.block, entry.offset, entry.size });
|
|
+ }
|
|
+ drain(s, device);
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/private-include/indium/outline-chain.private.hpp b/private-include/indium/outline-chain.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/outline-chain.private.hpp
|
|
@@ -0,0 +1,124 @@
|
|
+#pragma once
|
|
+
|
|
+// League's hover/target outline is a chain of full-screen 1080p passes that ping-pong between two textures: a mask pass,
|
|
+// then N passes with one fragment shader (SPIR-V hash 87819980) and M passes with a second (f61b5318), then a composite.
|
|
+// Every pass reads the previous result, so a pass can be dropped without breaking the data flow: the next pass simply reads
|
|
+// a result with fewer iterations applied. This keeps only the first 2*K passes of the first phase of each chain.
|
|
+//
|
|
+// Live settings (guest files, re-read every 2048 draws; create them from inside the guest):
|
|
+// /tmp/metal-outline-pairs K1: passes of the first phase kept = 2*K1 (default 0; 16 keeps everything)
|
|
+// /tmp/metal-outline-pairs2 K2: passes of the second phase kept = 2*K2 (default 16 = all of them)
|
|
+// Header-only with function-local statics (no class gains a member).
|
|
+
|
|
+#include <atomic>
|
|
+#include <cstdint>
|
|
+#include <mutex>
|
|
+#include <unordered_map>
|
|
+#include <unistd.h>
|
|
+#include <cstdio>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace OutlineChain {
|
|
+ struct State {
|
|
+ std::mutex mutex;
|
|
+ std::unordered_map<const void*, uint32_t> libraryHash;
|
|
+ std::unordered_map<const void*, uint8_t> pipelineKind; // 1 = first phase shader, 2 = second phase shader
|
|
+ };
|
|
+
|
|
+ inline State& state() {
|
|
+ static State* instance = new State;
|
|
+ return *instance;
|
|
+ }
|
|
+
|
|
+ inline void noteLibrary(const void* library, const char* data, size_t length) {
|
|
+ uint64_t h = 1469598103934665603ull;
|
|
+ for (size_t i = 0; i < length; ++i) { h ^= (unsigned char) data[i]; h *= 1099511628211ull; }
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ s.libraryHash[library] = (uint32_t) (h & 0xffffffffull);
|
|
+ }
|
|
+
|
|
+ inline void notePipeline(const void* pipeline, const void* fragmentLibrary) {
|
|
+ if (!fragmentLibrary) return;
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.libraryHash.find(fragmentLibrary);
|
|
+ if (it == s.libraryHash.end()) return;
|
|
+ if (it->second == 0x87819980u) s.pipelineKind[pipeline] = 1;
|
|
+ else if (it->second == 0xf61b5318u) s.pipelineKind[pipeline] = 2;
|
|
+ }
|
|
+
|
|
+ inline void readSettings(int& k1, int& k2) {
|
|
+ static std::atomic<int> v1 {0}, v2 {16};
|
|
+ static std::atomic<unsigned> calls {0};
|
|
+ if ((calls.fetch_add(1) & 2047) == 0) { // each read is two fopen()s through Darling's path translation
|
|
+ auto read = [](const char* path, int fallback) {
|
|
+ int out = fallback;
|
|
+ if (FILE* f = fopen(path, "r")) {
|
|
+ int v = -1;
|
|
+ if (fscanf(f, "%d", &v) == 1 && v >= 0 && v <= 16) out = v;
|
|
+ fclose(f);
|
|
+ }
|
|
+ return out;
|
|
+ };
|
|
+ v1.store(read("/tmp/metal-outline-pairs", 0));
|
|
+ v2.store(read("/tmp/metal-outline-pairs2", 16));
|
|
+ }
|
|
+ k1 = v1.load();
|
|
+ k2 = v2.load();
|
|
+ }
|
|
+
|
|
+ inline void report(int k, uint8_t kind, bool skipped, size_t vertexCount) {
|
|
+ static std::atomic<unsigned long> draws {0}, kind1 {0}, kind1v4 {0}, kind2 {0}, skippedDraws {0};
|
|
+ static std::atomic<unsigned> tick {0};
|
|
+ ++draws;
|
|
+ if (kind == 1) ++kind1;
|
|
+ if (kind == 1 && vertexCount == 4) ++kind1v4;
|
|
+ if (kind == 2) ++kind2;
|
|
+ if (skipped) ++skippedDraws;
|
|
+ if ((tick.fetch_add(1) & 1023) == 0) {
|
|
+ auto& s = state();
|
|
+ size_t kinds = 0, libs = 0;
|
|
+ { std::lock_guard<std::mutex> lock(s.mutex); kinds = s.pipelineKind.size(); libs = s.libraryHash.size(); }
|
|
+ char path[64];
|
|
+ snprintf(path, sizeof(path), "/tmp/indium-outline-%d.log", (int) getpid());
|
|
+ if (FILE* f = fopen(path, "w")) {
|
|
+ fprintf(f, "k=%d draws=%lu kind1=%lu kind1_v4=%lu kind2=%lu skipped=%lu pipelineKinds=%zu libraries=%zu\n", k,
|
|
+ draws.load(), kind1.load(), kind1v4.load(), kind2.load(), skippedDraws.load(), kinds, libs);
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // called for every drawPrimitives; true: skip this draw
|
|
+ inline bool shouldSkipDraw(const void* pipeline, size_t vertexCount) {
|
|
+ static thread_local unsigned run1 = 0, run2 = 0;
|
|
+ int k1 = 0, k2 = 16;
|
|
+ readSettings(k1, k2);
|
|
+ uint8_t kind = 0;
|
|
+ {
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ auto it = s.pipelineKind.find(pipeline);
|
|
+ if (it != s.pipelineKind.end()) kind = it->second;
|
|
+ }
|
|
+ if (vertexCount != 4 || kind == 0) {
|
|
+ // any other draw ends the chain
|
|
+ run1 = run2 = 0;
|
|
+ return false;
|
|
+ }
|
|
+ bool skip;
|
|
+ if (kind == 1) {
|
|
+ // a new chain starts with its first-phase passes
|
|
+ if (run2 != 0) { run1 = run2 = 0; }
|
|
+ skip = run1 >= (unsigned) (2 * k1);
|
|
+ ++run1;
|
|
+ } else {
|
|
+ skip = run2 >= (unsigned) (2 * k2);
|
|
+ ++run2;
|
|
+ }
|
|
+ // report(k1, kind, skip, vertexCount); // diagnostic: rewrote a guest log file every 1024 draws
|
|
+ return skip;
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/private-include/indium/render-pass-key.private.hpp b/private-include/indium/render-pass-key.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/render-pass-key.private.hpp
|
|
@@ -0,0 +1,56 @@
|
|
+#pragma once
|
|
+#include <vulkan/vulkan.h>
|
|
+#include <array>
|
|
+#include <algorithm>
|
|
+#include <optional>
|
|
+
|
|
+namespace Indium {
|
|
+// Exact descriptions, rather than Vulkan compatibility, determine reuse: load
|
|
+// actions and layouts affect preservation of the game's attachment contents.
|
|
+struct RenderPassKey {
|
|
+ std::array<uint32_t, 256> words {};
|
|
+ size_t size = 0;
|
|
+ bool operator==(const RenderPassKey& other) const {
|
|
+ return size == other.size && std::equal(words.begin(), words.begin() + size, other.words.begin());
|
|
+ }
|
|
+ struct Hash {
|
|
+ size_t operator()(const RenderPassKey& key) const {
|
|
+ size_t result = 14695981039346656037ull;
|
|
+ for (size_t i = 0; i < key.size; ++i) result = (result ^ key.words[i]) * 1099511628211ull;
|
|
+ return result;
|
|
+ }
|
|
+ };
|
|
+};
|
|
+inline std::optional<RenderPassKey> renderPassKey(const VkRenderPassCreateInfo& info) {
|
|
+ // The encoder emits one subpass without input/resolve/preserve attachments.
|
|
+ // Unsupported structures retain the original uncached Vulkan path.
|
|
+ if (info.pNext || info.subpassCount != 1 || info.attachmentCount > 16 || info.dependencyCount > 2)
|
|
+ return std::nullopt;
|
|
+ const auto& subpass = info.pSubpasses[0];
|
|
+ if (subpass.inputAttachmentCount || subpass.pResolveAttachments || subpass.preserveAttachmentCount || subpass.colorAttachmentCount > 16)
|
|
+ return std::nullopt;
|
|
+ RenderPassKey key;
|
|
+ auto add = [&](uint32_t word) { key.words[key.size++] = word; };
|
|
+ add(info.flags); add(info.attachmentCount);
|
|
+ for (uint32_t i = 0; i < info.attachmentCount; ++i) {
|
|
+ const auto& a = info.pAttachments[i];
|
|
+ add(a.flags); add(a.format); add(a.samples); add(a.loadOp); add(a.storeOp);
|
|
+ add(a.stencilLoadOp); add(a.stencilStoreOp); add(a.initialLayout); add(a.finalLayout);
|
|
+ }
|
|
+ add(subpass.flags); add(subpass.pipelineBindPoint); add(subpass.colorAttachmentCount);
|
|
+ for (uint32_t i = 0; i < subpass.colorAttachmentCount; ++i) {
|
|
+ add(subpass.pColorAttachments[i].attachment); add(subpass.pColorAttachments[i].layout);
|
|
+ }
|
|
+ add(subpass.pDepthStencilAttachment != nullptr);
|
|
+ if (subpass.pDepthStencilAttachment) {
|
|
+ add(subpass.pDepthStencilAttachment->attachment); add(subpass.pDepthStencilAttachment->layout);
|
|
+ }
|
|
+ add(info.dependencyCount);
|
|
+ for (uint32_t i = 0; i < info.dependencyCount; ++i) {
|
|
+ const auto& d = info.pDependencies[i];
|
|
+ add(d.srcSubpass); add(d.dstSubpass); add(d.srcStageMask); add(d.dstStageMask);
|
|
+ add(d.srcAccessMask); add(d.dstAccessMask); add(d.dependencyFlags);
|
|
+ }
|
|
+ return key;
|
|
+}
|
|
+}
|
|
diff --git a/private-include/indium/resource-owner-cache.private.hpp b/private-include/indium/resource-owner-cache.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/resource-owner-cache.private.hpp
|
|
@@ -0,0 +1,62 @@
|
|
+#pragma once
|
|
+
|
|
+#include <cstdint>
|
|
+#include <memory>
|
|
+#include <utility>
|
|
+#include <vector>
|
|
+
|
|
+namespace Indium {
|
|
+ // Encoder-local, insert-only cache. Contiguous owners avoid allocating and
|
|
+ // freeing a hash node for every resource. Owners are never erased while
|
|
+ // recorded GPU commands can reference them, so pointer identities stay valid.
|
|
+ template<class Base, class Private> class ResourceOwnerCache {
|
|
+ public:
|
|
+ struct Entry {
|
|
+ std::shared_ptr<Base> owner;
|
|
+ Private* value = nullptr;
|
|
+ bool depthFeedback = false;
|
|
+ };
|
|
+ private:
|
|
+ std::vector<Entry> _owners;
|
|
+ std::vector<size_t> _buckets; // owner index + 1; zero is an empty bucket
|
|
+
|
|
+ static size_t hash(Base* object) {
|
|
+ uint64_t value = reinterpret_cast<uintptr_t>(object);
|
|
+ value ^= value >> 30;
|
|
+ value *= UINT64_C(0xbf58476d1ce4e5b9);
|
|
+ value ^= value >> 27;
|
|
+ return static_cast<size_t>(value);
|
|
+ }
|
|
+ void grow() {
|
|
+ std::vector<size_t> buckets(_buckets.empty() ? 16 : _buckets.size() * 2, 0);
|
|
+ for (size_t i = 0; i < _owners.size(); ++i) {
|
|
+ size_t slot = hash(_owners[i].owner.get()) & (buckets.size() - 1);
|
|
+ while (buckets[slot]) slot = (slot + 1) & (buckets.size() - 1);
|
|
+ buckets[slot] = i + 1;
|
|
+ }
|
|
+ _buckets.swap(buckets);
|
|
+ }
|
|
+ public:
|
|
+ size_t size() const { return _owners.size(); }
|
|
+ // Returned pointers are valid until the next insertion, like vector
|
|
+ // elements. Callers copy the resolved pointer into their binding slot.
|
|
+ std::pair<Entry*, bool> acquire(const std::shared_ptr<Base>& owner) {
|
|
+ if (!owner) return {nullptr, false};
|
|
+ if (_buckets.empty()) grow();
|
|
+ size_t slot = hash(owner.get()) & (_buckets.size() - 1);
|
|
+ while (_buckets[slot]) {
|
|
+ auto& entry = _owners[_buckets[slot] - 1];
|
|
+ if (entry.owner.get() == owner.get()) return {&entry, false};
|
|
+ slot = (slot + 1) & (_buckets.size() - 1);
|
|
+ }
|
|
+ if ((_owners.size() + 1) * 2 > _buckets.size()) {
|
|
+ grow();
|
|
+ slot = hash(owner.get()) & (_buckets.size() - 1);
|
|
+ while (_buckets[slot]) slot = (slot + 1) & (_buckets.size() - 1);
|
|
+ }
|
|
+ _owners.push_back(Entry{owner, dynamic_cast<Private*>(owner.get()), false});
|
|
+ _buckets[slot] = _owners.size();
|
|
+ return {&_owners.back(), true};
|
|
+ }
|
|
+ };
|
|
+}
|
|
diff --git a/private-include/indium/retirement-queue.private.hpp b/private-include/indium/retirement-queue.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/retirement-queue.private.hpp
|
|
@@ -0,0 +1,63 @@
|
|
+#pragma once
|
|
+
|
|
+#include <array>
|
|
+#include <condition_variable>
|
|
+#include <mutex>
|
|
+#include <optional>
|
|
+#include <thread>
|
|
+#include <utility>
|
|
+
|
|
+namespace Indium {
|
|
+ // Completed GPU work only. A full queue falls back to synchronous cleanup,
|
|
+ // bounding retained resources even if the worker cannot keep up.
|
|
+ template<class Job, size_t capacity = 32> class RetirementQueue {
|
|
+ std::array<std::optional<Job>, capacity> _jobs;
|
|
+ size_t _head = 0, _count = 0;
|
|
+ bool _stopping = false;
|
|
+ std::mutex _mutex;
|
|
+ std::condition_variable _ready;
|
|
+ std::thread _worker;
|
|
+
|
|
+ void run() {
|
|
+ for (;;) {
|
|
+ std::optional<Job> job;
|
|
+ {
|
|
+ std::unique_lock<std::mutex> lock(_mutex);
|
|
+ _ready.wait(lock, [&] { return _count || _stopping; });
|
|
+ if (!_count && _stopping) return;
|
|
+ job = std::move(_jobs[_head]);
|
|
+ _jobs[_head].reset();
|
|
+ _head = (_head + 1) % capacity;
|
|
+ --_count;
|
|
+ }
|
|
+ // Resource destructors and driver calls never hold the queue lock.
|
|
+ (*job)();
|
|
+ }
|
|
+ }
|
|
+ public:
|
|
+ RetirementQueue() : _worker([this] { run(); }) {}
|
|
+ RetirementQueue(const RetirementQueue&) = delete;
|
|
+ ~RetirementQueue() { shutdown(); }
|
|
+
|
|
+ bool tryPush(Job&& job) {
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_mutex);
|
|
+ if (_stopping || _count == capacity) return false;
|
|
+ _jobs[(_head + _count) % capacity].emplace(std::move(job));
|
|
+ ++_count;
|
|
+ }
|
|
+ _ready.notify_one();
|
|
+ return true;
|
|
+ }
|
|
+ // Called before Vulkan/device teardown, while all driver entry points
|
|
+ // remain valid. Drain queued jobs before joining the worker.
|
|
+ void shutdown() {
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(_mutex);
|
|
+ _stopping = true;
|
|
+ }
|
|
+ _ready.notify_one();
|
|
+ if (_worker.joinable()) _worker.join();
|
|
+ }
|
|
+ };
|
|
+}
|
|
diff --git a/private-include/indium/submission-batch.private.hpp b/private-include/indium/submission-batch.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/submission-batch.private.hpp
|
|
@@ -0,0 +1,54 @@
|
|
+#pragma once
|
|
+#include <vulkan/vulkan.h>
|
|
+#include <array>
|
|
+#include <cassert>
|
|
+#include <vector>
|
|
+
|
|
+namespace Indium {
|
|
+ // Protected by the device graphics-queue mutex. Each packet preserves the
|
|
+ // original submit boundary and its semaphore waits/signals. Only the host
|
|
+ // driver call is combined; commands are not reordered or merged.
|
|
+ class SubmissionBatch {
|
|
+ static constexpr size_t capacity = 128;
|
|
+ struct Packet {
|
|
+ VkSubmitFlags flags = 0;
|
|
+ VkCommandBufferSubmitInfo command {};
|
|
+ std::vector<VkSemaphoreSubmitInfo> waits, signals;
|
|
+ };
|
|
+ std::array<Packet, capacity> _packets;
|
|
+ size_t _count = 0;
|
|
+ public:
|
|
+ bool empty() const { return _count == 0; }
|
|
+ bool full() const { return _count == capacity; }
|
|
+ void append(const VkSubmitInfo2& info) {
|
|
+ assert(!full() && !info.pNext && info.commandBufferInfoCount == 1);
|
|
+ auto& packet = _packets[_count++];
|
|
+ packet.flags = info.flags;
|
|
+ packet.command = info.pCommandBufferInfos[0];
|
|
+ packet.waits.clear(); packet.signals.clear();
|
|
+ if (info.waitSemaphoreInfoCount)
|
|
+ packet.waits.assign(info.pWaitSemaphoreInfos, info.pWaitSemaphoreInfos + info.waitSemaphoreInfoCount);
|
|
+ if (info.signalSemaphoreInfoCount)
|
|
+ packet.signals.assign(info.pSignalSemaphoreInfos, info.pSignalSemaphoreInfos + info.signalSemaphoreInfoCount);
|
|
+ }
|
|
+ template<class Submit> VkResult flush(Submit&& submit) {
|
|
+ if (empty()) return VK_SUCCESS;
|
|
+ std::array<VkSubmitInfo2, capacity> infos {};
|
|
+ for (size_t i = 0; i < _count; ++i) {
|
|
+ const auto& packet = _packets[i];
|
|
+ auto& info = infos[i];
|
|
+ info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
|
|
+ info.flags = packet.flags;
|
|
+ info.commandBufferInfoCount = 1;
|
|
+ info.pCommandBufferInfos = &packet.command;
|
|
+ info.waitSemaphoreInfoCount = packet.waits.size();
|
|
+ info.pWaitSemaphoreInfos = packet.waits.data();
|
|
+ info.signalSemaphoreInfoCount = packet.signals.size();
|
|
+ info.pSignalSemaphoreInfos = packet.signals.data();
|
|
+ }
|
|
+ VkResult result = submit(static_cast<uint32_t>(_count), infos.data());
|
|
+ if (result == VK_SUCCESS) _count = 0;
|
|
+ return result;
|
|
+ }
|
|
+ };
|
|
+}
|
|
diff --git a/private-include/indium/submit-batch.private.hpp b/private-include/indium/submit-batch.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/submit-batch.private.hpp
|
|
@@ -0,0 +1,12 @@
|
|
+#pragma once
|
|
+
|
|
+// Merging consecutive command buffer submissions into one vkQueueSubmit2 batch (see command-buffer.cpp).
|
|
+// League commits ~30 small command buffers per frame; each used to cost its own queue submit (an amdgpu CS ioctl plus
|
|
+// timeline-semaphore ioctls). Opt-in: guest flag file /tmp/metal-submit-batch (checked once).
|
|
+// A pending batch is flushed before anything that waits for or depends on submitted work (presenting commit, waitUntilCompleted,
|
|
+// texture uploads) and by a 2 ms timer.
|
|
+
|
|
+namespace Indium {
|
|
+ bool submitBatchEnabled();
|
|
+ void flushPendingSubmits();
|
|
+}
|
|
diff --git a/private-include/indium/submit-thread.private.hpp b/private-include/indium/submit-thread.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/submit-thread.private.hpp
|
|
@@ -0,0 +1,159 @@
|
|
+#pragma once
|
|
+
|
|
+// Ordered queue-submission worker.
|
|
+//
|
|
+// PrivateCommandBuffer::commit() used to do all its Vulkan queue work (semaphore reservation, vkQueueSubmit2, drawable
|
|
+// presentation) on the game's main thread: about 1.9 ms per frame in League's Baron fight, 77% of it inside the kernel's
|
|
+// amdgpu CS ioctl. The caller thread now only finishes recording; one FIFO worker performs the queue work, so the
|
|
+// submission order equals the commit order (timeline values are reserved inside the same task, in that order).
|
|
+// Every other path that touches the graphics queue (texture uploads) calls drain() first so it cannot overtake a
|
|
+// pending commit. Opt-in: guest flag file /tmp/metal-submit-thread (checked once) enables it; the A/B showed no gain, so submission stays synchronous.
|
|
+// Header-only with function-local statics: no class gains a member.
|
|
+
|
|
+#include <indium/waits.private.hpp>
|
|
+
|
|
+#include <condition_variable>
|
|
+#include <deque>
|
|
+#include <functional>
|
|
+#include <mutex>
|
|
+#include <thread>
|
|
+#include <unistd.h>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace SubmitThread {
|
|
+ struct State {
|
|
+ std::mutex mutex;
|
|
+ std::condition_variable wake;
|
|
+ std::condition_variable idle;
|
|
+ std::deque<std::function<void()>> queue;
|
|
+ size_t active = 0;
|
|
+ bool started = false;
|
|
+ const bool enabled = access("/tmp/metal-submit-thread", F_OK) == 0;
|
|
+ };
|
|
+
|
|
+ // One-shot completion token for a queued task: the worker signals it at the end of the task, so a caller that
|
|
+ // only has to wait for *its own* commit (the present dispatch it just queued) does not wait for the tasks that
|
|
+ // were queued after it. The token must outlive the task, which the waiting caller guarantees.
|
|
+ struct Token {
|
|
+ std::mutex mutex;
|
|
+ std::condition_variable condvar;
|
|
+ bool done = false;
|
|
+ void signal() {
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(mutex);
|
|
+ done = true;
|
|
+ }
|
|
+ condvar.notify_all();
|
|
+ }
|
|
+ // blocks until the task carrying this token has finished (counting the block under `site`)
|
|
+ void wait(const char* site) {
|
|
+ std::unique_lock<std::mutex> lock(mutex);
|
|
+ Waits::blockOn(site, condvar, lock, [this] { return done; });
|
|
+ }
|
|
+ };
|
|
+
|
|
+ inline State& state() {
|
|
+ static State* instance = new State;
|
|
+ return *instance;
|
|
+ }
|
|
+
|
|
+ inline bool& workerThread() {
|
|
+ static thread_local bool value = false;
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ inline bool onWorker() { return workerThread(); }
|
|
+
|
|
+ inline void loop() {
|
|
+ workerThread() = true;
|
|
+ auto& s = state();
|
|
+ for (;;) {
|
|
+ std::function<void()> task;
|
|
+ {
|
|
+ std::unique_lock<std::mutex> lock(s.mutex);
|
|
+ s.wake.wait(lock, [&] { return !s.queue.empty(); });
|
|
+ task = std::move(s.queue.front());
|
|
+ s.queue.pop_front();
|
|
+ ++s.active;
|
|
+ }
|
|
+ task();
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ --s.active;
|
|
+ if (s.queue.empty() && s.active == 0) s.idle.notify_all();
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // run `task` after every task queued before it; synchronously when disabled or already on the worker
|
|
+ inline void run(std::function<void()> task) {
|
|
+ auto& s = state();
|
|
+ if (!s.enabled || onWorker()) {
|
|
+ task();
|
|
+ return;
|
|
+ }
|
|
+ bool needsWake;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ if (!s.started) {
|
|
+ s.started = true;
|
|
+ std::thread thread(loop);
|
|
+ thread.detach();
|
|
+ }
|
|
+ // An active worker checks the queue before sleeping again. Only
|
|
+ // the idle/empty transition needs a wake; test under the same
|
|
+ // mutex as the worker's wait to avoid losing that transition.
|
|
+ needsWake = s.active == 0 && s.queue.empty();
|
|
+ s.queue.push_back(std::move(task));
|
|
+ }
|
|
+ if (needsWake) s.wake.notify_one();
|
|
+ }
|
|
+
|
|
+ // start the worker now (called when the device is created): a thread created lazily later would inherit the
|
|
+ // affinity of whichever thread happened to create it (the game's main thread is pinned to its own core)
|
|
+ inline void ensureStarted() {
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ if (!s.started) {
|
|
+ s.started = true;
|
|
+ std::thread thread(loop);
|
|
+ thread.detach();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // like run(), but always on the worker (deferred render encoding uses the same FIFO, regardless of the opt-in flag)
|
|
+ inline void post(std::function<void()> task) {
|
|
+ auto& s = state();
|
|
+ bool needsWake;
|
|
+ {
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ if (!s.started) {
|
|
+ s.started = true;
|
|
+ std::thread thread(loop);
|
|
+ thread.detach();
|
|
+ }
|
|
+ // An active worker checks the queue before sleeping again. Only
|
|
+ // the idle/empty transition needs a wake; test under the same
|
|
+ // mutex as the worker's wait to avoid losing that transition.
|
|
+ needsWake = s.active == 0 && s.queue.empty();
|
|
+ s.queue.push_back(std::move(task));
|
|
+ }
|
|
+ if (needsWake) s.wake.notify_one();
|
|
+ }
|
|
+
|
|
+ // true while the worker has queued or running tasks
|
|
+ inline bool pending() {
|
|
+ auto& s = state();
|
|
+ std::lock_guard<std::mutex> lock(s.mutex);
|
|
+ return !s.queue.empty() || s.active != 0;
|
|
+ }
|
|
+
|
|
+ // block until every task queued so far has finished (no-op on the worker itself)
|
|
+ inline void drain(const char* site = "drain") {
|
|
+ auto& s = state();
|
|
+ if (onWorker()) return;
|
|
+ std::unique_lock<std::mutex> lock(s.mutex);
|
|
+ Waits::blockOn(site, s.idle, lock, [&] { return s.queue.empty() && s.active == 0; });
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/include/indium/blit-command-encoder.hpp b/include/indium/blit-command-encoder.hpp
|
|
index b000d25..98baa9b 100644
|
|
--- a/include/indium/blit-command-encoder.hpp
|
|
+++ b/include/indium/blit-command-encoder.hpp
|
|
@@ -28,13 +28,13 @@ namespace Indium {
|
|
public:
|
|
virtual ~BlitCommandEncoder() = 0;
|
|
|
|
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t size) = 0;
|
|
- virtual void copy(std::shared_ptr<Buffer> source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) = 0;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Buffer> destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) = 0;
|
|
- virtual void copy(std::shared_ptr<Texture> source, std::shared_ptr<Texture> destination) = 0;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) = 0;
|
|
- virtual void copy(std::shared_ptr<Texture> source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, std::shared_ptr<Texture> destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) = 0;
|
|
- virtual void fillBuffer(std::shared_ptr<Buffer> buffer, Range<size_t> range, uint8_t value) = 0;
|
|
- virtual void generateMipmapsForTexture(std::shared_ptr<Texture> texture) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) = 0;
|
|
+ virtual void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) = 0;
|
|
+ virtual void fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) = 0;
|
|
+ virtual void generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) = 0;
|
|
};
|
|
};
|
|
diff --git a/private-include/indium/deferred-blit-encoder.private.hpp b/private-include/indium/deferred-blit-encoder.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/deferred-blit-encoder.private.hpp
|
|
@@ -0,0 +1,239 @@
|
|
+#pragma once
|
|
+
|
|
+// Deferred blit-command encoding.
|
|
+//
|
|
+// A blit encoder used to record its vkCmd* calls (and their barriers) directly into the command buffer, on the game's
|
|
+// thread, after waiting for every pending render replay of that command buffer (PrivateCommandBuffer::waitDeferred()).
|
|
+// Both the wait and the driver calls are on the frame's critical path, and nothing about them has to happen there:
|
|
+// Metal only requires that the calls are applied in order before the command buffer is committed.
|
|
+//
|
|
+// This facade implements the same public BlitCommandEncoder interface by recording the calls into a compact op list
|
|
+// (holding a reference to every buffer and texture they mention) and, at endEncoding(), posting the list to the ordered
|
|
+// worker of submit-thread.private.hpp -- the same FIFO the deferred render chunks and the commit itself run on. The
|
|
+// real PrivateBlitCommandEncoder is created, fed and ended there, in command-buffer order, before the commit that
|
|
+// closes the Vulkan command buffer. The deferred count keeps waitDeferred() correct for anything that still records
|
|
+// directly (compute encoders, and every encoder while deferred render encoding is off).
|
|
+//
|
|
+// Only used while deferred render encoding is on: with it off, render encoders record into the Vulkan command buffer
|
|
+// on the calling thread, so a blit replay has to stay on that thread too (the direct path keeps its waitDeferred()).
|
|
+// OFF by default (round-3 behaviour: blit encoders record directly on the calling thread after waitDeferred()). Guest
|
|
+// flag file /tmp/metal-deferred-blit (checked once per process) turns the facade on. Round 4 had it on by default; that
|
|
+// round regressed in matches, so it is an opt-in experiment now.
|
|
+
|
|
+#include <indium/blit-command-encoder.private.hpp>
|
|
+#include <indium/buffer.hpp>
|
|
+#include <indium/command-buffer.private.hpp>
|
|
+#include <indium/submit-thread.private.hpp>
|
|
+#include <indium/texture.hpp>
|
|
+
|
|
+#include <atomic>
|
|
+#include <cstdio>
|
|
+#include <memory>
|
|
+#include <stdexcept>
|
|
+#include <unistd.h>
|
|
+#include <vector>
|
|
+
|
|
+namespace Indium {
|
|
+ namespace AsyncEncode {
|
|
+ inline bool deferBlitEnabled() {
|
|
+ static const bool value = access("/tmp/metal-deferred-blit", F_OK) == 0;
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ // Blits are short lists of large operations; one chunk per encoder (posted at endEncoding) is enough, and it
|
|
+ // keeps the whole pass on one worker task, i.e. in one command-buffer order.
|
|
+ enum class BlitKind: uint8_t {
|
|
+ BufferToBuffer, BufferToTexture, TextureToBuffer, TextureToTexture, TextureToTextureLevels, TextureToTextureRegion, Mipmaps, FillBuffer,
|
|
+ };
|
|
+
|
|
+ struct BlitOp {
|
|
+ BlitKind kind = BlitKind::BufferToBuffer;
|
|
+ uint8_t value = 0;
|
|
+ // per-kind arguments, in the order of the call (offsets, slices, levels, row/image strides)
|
|
+ size_t n[8] = {0, 0, 0, 0, 0, 0, 0, 0};
|
|
+ Origin origin {};
|
|
+ Origin destinationOrigin {};
|
|
+ Size size {};
|
|
+ Range<size_t> range {0, 0};
|
|
+ std::shared_ptr<void> source;
|
|
+ std::shared_ptr<void> destination;
|
|
+ };
|
|
+
|
|
+ // One blit pass being recorded. `real` and `failed` are only touched by the worker.
|
|
+ struct BlitState {
|
|
+ std::shared_ptr<PrivateCommandBuffer> commandBuffer;
|
|
+ BlitPassDescriptor descriptor;
|
|
+ std::shared_ptr<PrivateBlitCommandEncoder> real;
|
|
+ bool failed = false;
|
|
+ BlitState(std::shared_ptr<PrivateCommandBuffer> cb, const BlitPassDescriptor& d): commandBuffer(std::move(cb)), descriptor(d) {}
|
|
+ };
|
|
+
|
|
+ inline void blitLog(const char* message) {
|
|
+ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) {
|
|
+ fprintf(f, "async-blit: %s\n", message);
|
|
+ fclose(f);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // Apply the recorded calls and end the real encoder. Runs on the worker.
|
|
+ inline void replayBlit(BlitState& st, std::vector<BlitOp>& ops) {
|
|
+ PoolScope pool;
|
|
+ if (!st.real && !st.failed) {
|
|
+ try {
|
|
+ st.real = st.commandBuffer->blitCommandEncoderNow(st.descriptor);
|
|
+ } catch (const std::exception& e) {
|
|
+ st.failed = true;
|
|
+ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-blit: encoder creation failed: %s\n", e.what()); fclose(f); }
|
|
+ }
|
|
+ }
|
|
+ if (st.real) {
|
|
+ PrivateBlitCommandEncoder* real = st.real.get();
|
|
+ for (BlitOp& op: ops) {
|
|
+ try {
|
|
+ switch (op.kind) {
|
|
+ case BlitKind::BufferToBuffer:
|
|
+ real->copy(std::static_pointer_cast<Buffer>(op.source), op.n[0], std::static_pointer_cast<Buffer>(op.destination), op.n[1], op.n[2]);
|
|
+ break;
|
|
+ case BlitKind::BufferToTexture:
|
|
+ real->copy(std::static_pointer_cast<Buffer>(op.source), op.n[0], op.n[1], op.n[2], op.size,
|
|
+ std::static_pointer_cast<Texture>(op.destination), op.n[3], op.n[4], op.origin);
|
|
+ break;
|
|
+ case BlitKind::TextureToBuffer:
|
|
+ real->copy(std::static_pointer_cast<Texture>(op.source), op.n[0], op.n[1], op.origin, op.size,
|
|
+ std::static_pointer_cast<Buffer>(op.destination), op.n[2], op.n[3], op.n[4]);
|
|
+ break;
|
|
+ case BlitKind::TextureToTexture:
|
|
+ real->copy(std::static_pointer_cast<Texture>(op.source), std::static_pointer_cast<Texture>(op.destination));
|
|
+ break;
|
|
+ case BlitKind::TextureToTextureLevels:
|
|
+ real->copy(std::static_pointer_cast<Texture>(op.source), op.n[0], op.n[1],
|
|
+ std::static_pointer_cast<Texture>(op.destination), op.n[2], op.n[3], op.n[4], op.n[5]);
|
|
+ break;
|
|
+ case BlitKind::TextureToTextureRegion:
|
|
+ real->copy(std::static_pointer_cast<Texture>(op.source), op.n[0], op.n[1], op.origin, op.size,
|
|
+ std::static_pointer_cast<Texture>(op.destination), op.n[2], op.n[3], op.destinationOrigin);
|
|
+ break;
|
|
+ case BlitKind::Mipmaps:
|
|
+ real->generateMipmapsForTexture(std::static_pointer_cast<Texture>(op.source));
|
|
+ break;
|
|
+ case BlitKind::FillBuffer:
|
|
+ real->fillBuffer(std::static_pointer_cast<Buffer>(op.source), op.range, op.value);
|
|
+ break;
|
|
+ }
|
|
+ } catch (const std::exception& e) {
|
|
+ if (FILE* f = fopen("/tmp/metal-trace.log", "a")) { fprintf(f, "async-blit: op %d failed: %s\n", (int) op.kind, e.what()); fclose(f); }
|
|
+ }
|
|
+ }
|
|
+ try { real->endEncoding(); } catch (...) {}
|
|
+ st.real.reset();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ class DeferredBlitCommandEncoder final: public BlitCommandEncoder {
|
|
+ std::shared_ptr<BlitState> _state;
|
|
+ std::vector<BlitOp> _ops;
|
|
+ bool _begun = false;
|
|
+ bool _ended = false;
|
|
+
|
|
+ // Hand everything recorded so far to the worker. The ops own the resources they reference, so the game may
|
|
+ // release them (and us) as soon as this returns.
|
|
+ void flush() {
|
|
+ auto st = _state;
|
|
+ auto cb = st->commandBuffer;
|
|
+ if (!_begun) { _begun = true; cb->deferredBegin(); }
|
|
+ auto ops = std::make_shared<std::vector<BlitOp>>(std::move(_ops));
|
|
+ _ops.clear();
|
|
+ if (!cb->replayAllowed()) {
|
|
+ // an encoder dropped after the commit: the Vulkan command buffer is closed, so balance the deferred
|
|
+ // count and drop the ops instead of recording into it
|
|
+ blitLog("encoder dropped after commit");
|
|
+ cb->deferredEnd();
|
|
+ return;
|
|
+ }
|
|
+ SubmitThread::post([st, cb, ops]() mutable {
|
|
+ replayBlit(*st, *ops);
|
|
+ st.reset();
|
|
+ cb->deferredEnd();
|
|
+ });
|
|
+ }
|
|
+
|
|
+ BlitOp& add(BlitKind kind) {
|
|
+ _ops.emplace_back();
|
|
+ BlitOp& op = _ops.back();
|
|
+ op.kind = kind;
|
|
+ return op;
|
|
+ }
|
|
+
|
|
+ public:
|
|
+ DeferredBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> cb, const BlitPassDescriptor& d):
|
|
+ _state(std::make_shared<BlitState>(std::move(cb), d)) {}
|
|
+ explicit DeferredBlitCommandEncoder(std::shared_ptr<PrivateCommandBuffer> cb):
|
|
+ DeferredBlitCommandEncoder(std::move(cb), BlitPassDescriptor {}) {}
|
|
+
|
|
+ ~DeferredBlitCommandEncoder() override { endEncoding(); }
|
|
+
|
|
+ void endEncoding() override {
|
|
+ if (_ended) return;
|
|
+ _ended = true;
|
|
+ flush();
|
|
+ _state.reset();
|
|
+ }
|
|
+
|
|
+ // The real encoder rejects blit options; doing it here keeps the exception on the calling thread.
|
|
+ static void rejectOptions(BlitOption options) {
|
|
+ if (options != BlitOption::None) throw std::runtime_error("TODO: support blit options");
|
|
+ }
|
|
+
|
|
+ void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t size) override {
|
|
+ BlitOp& op = add(BlitKind::BufferToBuffer);
|
|
+ op.source = source; op.destination = destination;
|
|
+ op.n[0] = sourceOffset; op.n[1] = destinationOffset; op.n[2] = size;
|
|
+ }
|
|
+ void copy(const std::shared_ptr<Buffer>& source, size_t sourceOffset, size_t sourceBytesPerRow, size_t sourceBytesPerImage, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin, BlitOption options = BlitOption::None) override {
|
|
+ rejectOptions(options);
|
|
+ BlitOp& op = add(BlitKind::BufferToTexture);
|
|
+ op.source = source; op.destination = destination;
|
|
+ op.n[0] = sourceOffset; op.n[1] = sourceBytesPerRow; op.n[2] = sourceBytesPerImage;
|
|
+ op.n[3] = destinationSlice; op.n[4] = destinationLevel;
|
|
+ op.origin = destinationOrigin; op.size = sourceSize;
|
|
+ }
|
|
+ void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Buffer>& destination, size_t destinationOffset, size_t destinationBytesPerRow, size_t destinationBytesPerImage, BlitOption options = BlitOption::None) override {
|
|
+ rejectOptions(options);
|
|
+ BlitOp& op = add(BlitKind::TextureToBuffer);
|
|
+ op.source = source; op.destination = destination;
|
|
+ op.n[0] = sourceSlice; op.n[1] = sourceLevel;
|
|
+ op.n[2] = destinationOffset; op.n[3] = destinationBytesPerRow; op.n[4] = destinationBytesPerImage;
|
|
+ op.origin = sourceOrigin; op.size = sourceSize;
|
|
+ }
|
|
+ void copy(const std::shared_ptr<Texture>& source, const std::shared_ptr<Texture>& destination) override {
|
|
+ BlitOp& op = add(BlitKind::TextureToTexture);
|
|
+ op.source = source; op.destination = destination;
|
|
+ }
|
|
+ void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, size_t sliceCount, size_t levelCount) override {
|
|
+ BlitOp& op = add(BlitKind::TextureToTextureLevels);
|
|
+ op.source = source; op.destination = destination;
|
|
+ op.n[0] = sourceSlice; op.n[1] = sourceLevel; op.n[2] = destinationSlice; op.n[3] = destinationLevel;
|
|
+ op.n[4] = sliceCount; op.n[5] = levelCount;
|
|
+ }
|
|
+ void copy(const std::shared_ptr<Texture>& source, size_t sourceSlice, size_t sourceLevel, Origin sourceOrigin, Size sourceSize, const std::shared_ptr<Texture>& destination, size_t destinationSlice, size_t destinationLevel, Origin destinationOrigin) override {
|
|
+ BlitOp& op = add(BlitKind::TextureToTextureRegion);
|
|
+ op.source = source; op.destination = destination;
|
|
+ op.n[0] = sourceSlice; op.n[1] = sourceLevel; op.n[2] = destinationSlice; op.n[3] = destinationLevel;
|
|
+ op.origin = sourceOrigin; op.destinationOrigin = destinationOrigin; op.size = sourceSize;
|
|
+ }
|
|
+ void fillBuffer(const std::shared_ptr<Buffer>& buffer, Range<size_t> range, uint8_t value) override {
|
|
+ // the real encoder rejects misaligned ranges; keep the exception on the calling thread
|
|
+ if ((range.start & 3) != 0) throw std::runtime_error("Misaligned fillBuffer offset");
|
|
+ if ((range.length & 3) != 0) throw std::runtime_error("Misaligned fillBuffer length");
|
|
+ BlitOp& op = add(BlitKind::FillBuffer);
|
|
+ op.source = buffer;
|
|
+ op.range = range;
|
|
+ op.value = value;
|
|
+ }
|
|
+ void generateMipmapsForTexture(const std::shared_ptr<Texture>& texture) override {
|
|
+ BlitOp& op = add(BlitKind::Mipmaps);
|
|
+ op.source = texture;
|
|
+ }
|
|
+ };
|
|
+ }
|
|
+}
|
|
diff --git a/private-include/indium/waits.private.hpp b/private-include/indium/waits.private.hpp
|
|
new file mode 100644
|
|
--- /dev/null
|
|
+++ b/private-include/indium/waits.private.hpp
|
|
@@ -0,0 +1,145 @@
|
|
+#pragma once
|
|
+
|
|
+// Wait-site counters.
|
|
+//
|
|
+// The main thread's frame budget is spent partly on blocking in our own hand-offs (the submit worker's drain, the
|
|
+// deferred-encoder sync, texture uploads) and in the compositor's drawable hand-out. A sample profile cannot attribute
|
|
+// that time (the blocked PC is inside a futex helper), so every wait site counts how often the calling thread *actually
|
|
+// blocked* and how long, and appends a summary to the guest's /tmp/metal-trace.log every 5 s.
|
|
+//
|
|
+// Everything here is always compiled in; the counters are only active when /tmp/metal-debug-waits exists when the
|
|
+// process starts (checked once). Disabled, each site costs one predictable branch: a call site must establish that it
|
|
+// is about to block (a false predicate under the lock, a free-drawable check) before it constructs a Block, and that
|
|
+// check is the only work done when the flag is absent.
|
|
+//
|
|
+// Header-only: the counter state is per dylib (each library writes its own summary), so no class gains a member. The
|
|
+// functions are declared hidden: an inline function's static locals are otherwise exported as weak definitions (even
|
|
+// under -fvisibility-inlines-hidden), and dyld coalesces those across images, so libindium and QuartzCore would share
|
|
+// one copy -- harmless only while both images were built from the same version of this header. Hidden keeps the state
|
|
+// in each image, as documented.
|
|
+//
|
|
+// Flag file pitfall (why a match can show no waits lines although the file "exists"): the guest /tmp is
|
|
+// ~/.darling/private/tmp *inside Darling's overlay mount*, i.e. the host path is the overlay's upper directory. A file
|
|
+// created there from the host while the Darling namespace is running is not guaranteed to be visible to guest
|
|
+// processes: once a guest process has looked the name up and got ENOENT, the overlay caches the negative entry and
|
|
+// keeps answering ENOENT. Create flag files from inside the guest (`darling shell touch /tmp/metal-debug-waits`) or
|
|
+// while Darling is shut down. When the counters are on, the first check writes a "waits enabled" line, so its absence
|
|
+// from /tmp/metal-trace.log means the flag was not seen.
|
|
+
|
|
+#include <chrono>
|
|
+#include <condition_variable>
|
|
+#include <cstdint>
|
|
+#include <cstdio>
|
|
+#include <cstdlib>
|
|
+#include <ctime>
|
|
+#include <map>
|
|
+#include <mutex>
|
|
+#include <string>
|
|
+#include <dlfcn.h>
|
|
+#include <time.h>
|
|
+#include <unistd.h>
|
|
+
|
|
+#define INDIUM_WAITS_LOCAL __attribute__((visibility("hidden")))
|
|
+
|
|
+namespace Indium {
|
|
+ namespace Waits {
|
|
+ // The summaries append to the guest's /tmp/metal-trace.log like the other diagnostics; DARLING_WAITS_LOG
|
|
+ // overrides the path so the host-side test does not append to the runtime's log. Only read when the counters
|
|
+ // are on (i.e. from the dump path).
|
|
+ INDIUM_WAITS_LOCAL inline const char* logPath() {
|
|
+ static const char* path = []() -> const char* {
|
|
+ const char* override = getenv("DARLING_WAITS_LOG");
|
|
+ return override && *override ? override : "/tmp/metal-trace.log";
|
|
+ }();
|
|
+ return path;
|
|
+ }
|
|
+
|
|
+ // Guest flag file, read once per process (per image) at the first wait site that is reached. When it is on, that
|
|
+ // first check appends one "waits enabled" line naming the image, so a match log proves the path is live even if
|
|
+ // no site ever blocks.
|
|
+ INDIUM_WAITS_LOCAL inline bool readEnabled() {
|
|
+ if (access("/tmp/metal-debug-waits", F_OK) != 0) return false;
|
|
+ Dl_info info {};
|
|
+ const char* image = dladdr(reinterpret_cast<void*>(&readEnabled), &info) && info.dli_fname ? info.dli_fname : "?";
|
|
+ if (FILE* log = fopen(logPath(), "a")) {
|
|
+ fprintf(log, "waits enabled@%ld: pid=%d image=%s\n", (long) time(nullptr), (int) getpid(), image);
|
|
+ fclose(log);
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+
|
|
+ INDIUM_WAITS_LOCAL inline bool enabled() {
|
|
+ static const bool value = readEnabled();
|
|
+ return value;
|
|
+ }
|
|
+
|
|
+ // Monotonic nanoseconds. steady_clock is what the rest of Indium uses (libc++ maps it to CLOCK_UPTIME_RAW, i.e.
|
|
+ // mach_absolute_time, on Darwin); clock_gettime(CLOCK_MONOTONIC) is wall-clock-derived in Darwin's libc
|
|
+ // (gettimeofday minus the boot time, with a kern.boottime sysctl fallback), so it is neither monotonic nor cheap
|
|
+ // under Darling.
|
|
+ INDIUM_WAITS_LOCAL inline uint64_t now() {
|
|
+ return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
|
+ std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
+ }
|
|
+
|
|
+ INDIUM_WAITS_LOCAL inline void record(const char* site, uint64_t nanoseconds) {
|
|
+ struct Stat { uint64_t blocks = 0, sum = 0, max = 0; };
|
|
+ static std::mutex lock;
|
|
+ static std::map<std::string, Stat> stats;
|
|
+ static time_t last = 0;
|
|
+ std::lock_guard<std::mutex> guard(lock);
|
|
+ Stat& stat = stats[site];
|
|
+ ++stat.blocks;
|
|
+ stat.sum += nanoseconds;
|
|
+ if (nanoseconds > stat.max) stat.max = nanoseconds;
|
|
+ time_t now = time(nullptr);
|
|
+ if (now - last >= 5) {
|
|
+ last = now;
|
|
+ if (FILE* log = fopen(logPath(), "a")) {
|
|
+ fprintf(log, "waits@%ld:", (long) now);
|
|
+ for (auto& entry: stats) {
|
|
+ fprintf(log, " %s[n=%llu sum=%.1fus max=%.1fus]", entry.first.c_str(),
|
|
+ (unsigned long long) entry.second.blocks, entry.second.sum / 1000.0, entry.second.max / 1000.0);
|
|
+ }
|
|
+ fprintf(log, "\n");
|
|
+ fclose(log);
|
|
+ }
|
|
+ stats.clear();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // One blocking episode of `site`. The constructor checks the flag and the destructor records (a no-op when the
|
|
+ // counter is off). Use it around a wait that is known to block; blockOn()/blockOnLock() do that check for the
|
|
+ // sites where the caller cannot know in advance.
|
|
+ struct INDIUM_WAITS_LOCAL Block {
|
|
+ const char* site;
|
|
+ uint64_t start = 0;
|
|
+ explicit Block(const char* site_): site(site_) { if (enabled()) start = now(); }
|
|
+ ~Block() { if (start != 0) record(site, now() - start); }
|
|
+ };
|
|
+
|
|
+ // Wait for `predicate` on `condvar`, counting the block only when the predicate is already false at the call
|
|
+ // (i.e. the wait really has to sleep). The predicate is re-checked by the wait itself, so both paths are safe.
|
|
+ template<class Predicate>
|
|
+ INDIUM_WAITS_LOCAL inline void blockOn(const char* site, std::condition_variable& condvar, std::unique_lock<std::mutex>& lock, Predicate predicate) {
|
|
+ if (!enabled() || predicate()) {
|
|
+ condvar.wait(lock, predicate);
|
|
+ return;
|
|
+ }
|
|
+ Block block(site);
|
|
+ condvar.wait(lock, predicate);
|
|
+ }
|
|
+
|
|
+ // Lock `mutex`, counting the block only when it is contended at the time of the call. try_lock only runs when
|
|
+ // the flag is on, so the disabled cost is the same single branch.
|
|
+ INDIUM_WAITS_LOCAL inline void blockOnLock(const char* site, std::mutex& mutex) {
|
|
+ if (!enabled()) {
|
|
+ mutex.lock();
|
|
+ return;
|
|
+ }
|
|
+ if (mutex.try_lock()) return;
|
|
+ Block block(site);
|
|
+ mutex.lock();
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
|
index 8d95863..1af3167 100644
|
|
--- a/CMakeLists.txt
|
|
+++ b/CMakeLists.txt
|
|
@@ -2,6 +2,41 @@ project(indium)
|
|
cmake_minimum_required(VERSION 3.20)
|
|
|
|
find_package(Vulkan REQUIRED)
|
|
+
|
|
+# Iridium compiles against these LLVM headers and loads the shared libLLVM of the same major at run time, so the LLVM
|
|
+# found here needs an llvm-config that reports a shared library. find_package also looks in <prefix>/llvm/lib/cmake/llvm
|
|
+# for every PATH entry, so with /opt/rocm/bin on PATH it picks ROCm's private LLVM ahead of the system one, and a cached
|
|
+# LLVM_DIR can name an LLVM that was removed since. Unless LLVM_DIR names a usable LLVM, use the llvm-config on PATH.
|
|
+function(indium_llvm_config_works tool result)
|
|
+ set(${result} FALSE PARENT_SCOPE)
|
|
+ execute_process(COMMAND "${tool}" --link-shared --libnames
|
|
+ OUTPUT_VARIABLE libname RESULT_VARIABLE status ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
+ if (status EQUAL 0 AND libname)
|
|
+ set(${result} TRUE PARENT_SCOPE)
|
|
+ endif()
|
|
+endfunction()
|
|
+
|
|
+set(indium_llvm_usable FALSE)
|
|
+if (LLVM_DIR)
|
|
+ # <prefix>/lib/cmake/llvm or <prefix>/lib64/cmake/llvm
|
|
+ get_filename_component(indium_llvm_prefix "${LLVM_DIR}/../../.." ABSOLUTE)
|
|
+ indium_llvm_config_works("${indium_llvm_prefix}/bin/llvm-config" indium_llvm_usable)
|
|
+endif()
|
|
+if (NOT indium_llvm_usable)
|
|
+ find_program(INDIUM_PATH_LLVM_CONFIG llvm-config)
|
|
+ if (INDIUM_PATH_LLVM_CONFIG)
|
|
+ indium_llvm_config_works("${INDIUM_PATH_LLVM_CONFIG}" indium_path_llvm_usable)
|
|
+ execute_process(COMMAND "${INDIUM_PATH_LLVM_CONFIG}" --cmakedir
|
|
+ OUTPUT_VARIABLE indium_path_llvm_dir RESULT_VARIABLE indium_status ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
|
|
+ if (indium_path_llvm_usable AND indium_status EQUAL 0 AND EXISTS "${indium_path_llvm_dir}")
|
|
+ if (LLVM_DIR)
|
|
+ message(STATUS "LLVM_DIR=${LLVM_DIR} has no llvm-config with a shared library; using ${indium_path_llvm_dir}")
|
|
+ endif()
|
|
+ set(LLVM_DIR "${indium_path_llvm_dir}" CACHE PATH "The directory containing a CMake configuration file for LLVM." FORCE)
|
|
+ endif()
|
|
+ endif()
|
|
+endif()
|
|
+
|
|
find_package(LLVM REQUIRED CONFIG)
|
|
|
|
if (DARLING)
|
|
@@ -158,11 +193,35 @@ if (NOT DARLING)
|
|
)
|
|
endif()
|
|
|
|
-execute_process(
|
|
- COMMAND llvm-config --link-shared --libnames
|
|
- OUTPUT_VARIABLE HOST_LLVM_LIBNAME
|
|
- OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
-)
|
|
+# Use an llvm-config of the same major as the headers found above. A stale cached LLVM_DIR can point at
|
|
+# a removed LLVM whose llvm-config exists but whose library is gone, so fall back to the one on PATH.
|
|
+set(HOST_LLVM_LIBNAME "")
|
|
+foreach(candidate IN ITEMS "${LLVM_TOOLS_BINARY_DIR}/llvm-config" llvm-config)
|
|
+ execute_process(
|
|
+ COMMAND "${candidate}" --version
|
|
+ OUTPUT_VARIABLE candidate_version
|
|
+ RESULT_VARIABLE candidate_status
|
|
+ ERROR_QUIET
|
|
+ OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
+ )
|
|
+ if (candidate_status EQUAL 0 AND candidate_version MATCHES "^${LLVM_VERSION_MAJOR}\\.")
|
|
+ execute_process(
|
|
+ COMMAND "${candidate}" --link-shared --libnames
|
|
+ OUTPUT_VARIABLE HOST_LLVM_LIBNAME
|
|
+ RESULT_VARIABLE candidate_status
|
|
+ ERROR_QUIET
|
|
+ OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
+ )
|
|
+ if (candidate_status EQUAL 0 AND HOST_LLVM_LIBNAME)
|
|
+ break()
|
|
+ endif()
|
|
+ set(HOST_LLVM_LIBNAME "")
|
|
+ endif()
|
|
+endforeach()
|
|
+if (NOT HOST_LLVM_LIBNAME)
|
|
+ message(FATAL_ERROR "No working llvm-config for LLVM ${LLVM_VERSION} (LLVM_DIR=${LLVM_DIR}). "
|
|
+ "Install an LLVM with a shared libLLVM, or pass -DLLVM_DIR=<its lib/cmake/llvm directory>.")
|
|
+endif()
|
|
|
|
target_compile_definitions(iridium PRIVATE
|
|
"HOST_LLVM_LIBNAME=\"${HOST_LLVM_LIBNAME}\""
|
|
diff --git a/src/iridium/dynamic-llvm.cpp b/src/iridium/dynamic-llvm.cpp
|
|
index 1b5876e..03af560 100644
|
|
--- a/src/iridium/dynamic-llvm.cpp
|
|
+++ b/src/iridium/dynamic-llvm.cpp
|
|
@@ -1,4 +1,7 @@
|
|
#include <iridium/dynamic-llvm.hpp>
|
|
+#include <llvm/Config/llvm-config.h>
|
|
+#include <stdexcept>
|
|
+#include <string>
|
|
|
|
#ifdef DARLING
|
|
#include <elfcalls.h>
|
|
@@ -19,7 +22,20 @@ bool Iridium::DynamicLLVM::init() {
|
|
libraryHandle = dlopen(HOST_LLVM_LIBNAME, RTLD_LAZY);
|
|
#endif
|
|
|
|
- return !!libraryHandle;
|
|
+ if (!libraryHandle) return false;
|
|
+ // LLVM's C API enum values changed in LLVM 23. A different runtime major
|
|
+ // misclassifies constants and branch instructions even when symbols resolve.
|
|
+ using GetVersion = void (*)(unsigned*, unsigned*, unsigned*);
|
|
+ auto getVersion = reinterpret_cast<GetVersion>(resolveSymbol("LLVMGetVersion"));
|
|
+ unsigned major = 0, minor = 0, patch = 0;
|
|
+ if (getVersion) getVersion(&major, &minor, &patch);
|
|
+ if (major != LLVM_VERSION_MAJOR) {
|
|
+ finit();
|
|
+ throw std::runtime_error("Iridium LLVM version mismatch: built with LLVM " +
|
|
+ std::to_string(LLVM_VERSION_MAJOR) + ", loaded LLVM " + std::to_string(major) +
|
|
+ " from " HOST_LLVM_LIBNAME "; rebuild and reinstall the runtime with matching LLVM headers and library");
|
|
+ }
|
|
+ return true;
|
|
};
|
|
|
|
void Iridium::DynamicLLVM::finit() {
|