17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-09 00:17:39 +01:00

Walk the shadow passes and the culled model groups on worker threads in the Vulkan renderer

This commit is contained in:
2026-10-27 20:56:11 +01:00
parent c44b243698
commit 1447dcf075
55 changed files with 4114 additions and 427 deletions

View File

@@ -141,7 +141,7 @@ bool vulkan14_renderer::compute_ao_lighting(std::size_t const Index)
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto const commands{ctx.commands};
auto const commands{m_commands};
if ((false == bool(targets.ssao)) || (false == bool(targets.ssao_blur)) || (false == bool(targets.scene_depth)))
{
return false;
@@ -187,7 +187,7 @@ void vulkan14_renderer::render_ao(std::size_t const Index, int const Mode)
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto const commands{ctx.commands};
auto const commands{m_commands};
if ((false == bool(targets.ssao)) || (false == bool(targets.ssao_blur)))
{
return;

View File

@@ -318,7 +318,7 @@ void vulkan14_renderer::render_cab_probe()
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &probe{scene.cabprobe};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto *vehicle{cab_probe_vehicle()};
glm::vec3 center{0.f};
glm::vec3 extent{0.f};

View File

@@ -227,6 +227,17 @@ void query_capabilities(VkPhysicalDevice Physical, feature_chain &Queried, capab
properties2.pNext = &properties14;
vkGetPhysicalDeviceProperties2(Physical, &properties2);
Caps.max_push_descriptors = properties14.maxPushDescriptors;
// the layouts the host can fill images in (hostImageCopy): the textures are filled straight to the one they're sampled in
std::vector<VkImageLayout> copylayouts(properties14.copyDstLayoutCount);
if (false == copylayouts.empty())
{
properties14.pCopyDstLayouts = copylayouts.data();
properties14.copySrcLayoutCount = 0;
properties14.pCopySrcLayouts = nullptr;
vkGetPhysicalDeviceProperties2(Physical, &properties2);
copylayouts.resize(properties14.copyDstLayoutCount);
Caps.host_copy_sampled = (std::find(copylayouts.begin(), copylayouts.end(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) != copylayouts.end());
}
}
// features
@@ -319,10 +330,10 @@ std::string capability_report(capabilities const &Caps)
line("driver " + Caps.driver_name + " " + Caps.driver_info);
line("device memory " + memory_size_string(Caps.device_local_memory));
line(std::string("draw indirect count ") + flag(Caps.draw_indirect_count));
line(std::string("host image copy ") + flag(Caps.host_image_copy) + (Caps.host_image_copy ? (Caps.host_copy_sampled ? " (to the sampled layout)" : " (not to the sampled layout)") : ""));
line(std::string("extended dynamic state3 ") + flag(Caps.ext_extended_dynamic_state3));
line(std::string("memory budget ") + flag(Caps.ext_memory_budget));
line(std::string("swapchain maintenance1 ") + flag(Caps.ext_swapchain_maintenance1));
line(std::string("host image copy ") + flag(Caps.host_image_copy));
line(std::string("sampler filter minmax ") + flag(Caps.sampler_filter_minmax));
line(std::string("shader output layer ") + flag(Caps.shader_output_layer));
line(std::string("fill mode non solid ") + flag(Caps.fill_mode_non_solid));

View File

@@ -89,6 +89,8 @@ struct capabilities
// optional features, each with a way without it
bool draw_indirect_count{false};
bool host_image_copy{false};
// the host fills images straight to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL (one of the copy layouts of 1.4)
bool host_copy_sampled{false};
bool fill_mode_non_solid{false};
bool pipeline_statistics_query{false};
bool sampler_filter_minmax{false};

View File

@@ -208,6 +208,55 @@ struct label_call
std::size_t name;
};
// the stage and the access of one side of a barrier, without what a queue of the compute family doesn't have (the async compute of M6)
void compute_queue_scope(VkPipelineStageFlags2 &Stage, VkAccessFlags2 &Access)
{
auto stage{Stage};
if ((stage & VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT) != 0)
{
stage = (stage & ~VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT) | VK_PIPELINE_STAGE_2_COPY_BIT | VK_PIPELINE_STAGE_2_CLEAR_BIT;
}
stage &= (VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT | VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT | VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_2_COPY_BIT |
VK_PIPELINE_STAGE_2_CLEAR_BIT | VK_PIPELINE_STAGE_2_HOST_BIT | VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT);
VkAccessFlags2 allowed{VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
if ((stage & VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT) != 0)
{
allowed |= VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT |
VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT | VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT | VK_ACCESS_2_HOST_READ_BIT | VK_ACCESS_2_HOST_WRITE_BIT;
}
if ((stage & VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) != 0)
{
allowed |= VK_ACCESS_2_UNIFORM_READ_BIT | VK_ACCESS_2_SHADER_READ_BIT | VK_ACCESS_2_SHADER_WRITE_BIT | VK_ACCESS_2_SHADER_SAMPLED_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_READ_BIT | VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT;
}
if ((stage & VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT) != 0)
{
allowed |= VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT;
}
if ((stage & VK_PIPELINE_STAGE_2_COPY_BIT) != 0)
{
allowed |= VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT;
}
if ((stage & VK_PIPELINE_STAGE_2_CLEAR_BIT) != 0)
{
allowed |= VK_ACCESS_2_TRANSFER_WRITE_BIT;
}
if ((stage & VK_PIPELINE_STAGE_2_HOST_BIT) != 0)
{
allowed |= VK_ACCESS_2_HOST_READ_BIT | VK_ACCESS_2_HOST_WRITE_BIT;
}
Stage = (stage != 0 ? stage : VK_PIPELINE_STAGE_2_NONE);
Access = (stage != 0 ? (Access & allowed) : VK_ACCESS_2_NONE);
}
template <typename Barrier_> void compute_queue_barriers(Barrier_ *Barriers, std::uint32_t const Count)
{
for (std::uint32_t i = 0; i < Count; ++i)
{
compute_queue_scope(Barriers[i].srcStageMask, Barriers[i].srcAccessMask);
compute_queue_scope(Barriers[i].dstStageMask, Barriers[i].dstAccessMask);
}
}
bool compute_stages(VkShaderStageFlags const Stages)
{
return (Stages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
@@ -231,6 +280,115 @@ void command_stream::clear()
m_work = 0;
m_calls = 0;
m_finished = false;
m_pieces.clear();
m_piecefirst = 0;
m_order.clear();
m_split = no_call;
m_splitchunk = 0;
}
void command_stream::split()
{
assert((m_rendering == no_call) && (m_queries == 0));
if (m_chunks.empty())
{
clear();
}
if (m_split != no_call)
{
return;
}
cut();
m_split = m_chunks.size() - 1;
}
void command_stream::splice(command_stream const &Other)
{
assert((m_rendering == no_call) && (m_queries == 0));
assert(Other.m_pieces.empty());
if (m_chunks.empty())
{
clear();
}
// the chunk under way ends here, with the part of the stream it closes and the state the part leaves
auto &current{m_chunks.back()};
current.end = m_data.size();
current.suspended = no_call;
piece own;
own.stream = this;
own.first = m_piecefirst;
own.end = m_chunks.size();
own.state.assign(m_state.begin(), m_state.end());
own.pushes = m_pushes;
m_pieces.push_back(std::move(own));
piece spliced;
spliced.stream = &Other;
m_pieces.push_back(std::move(spliced));
// the calls after it begin a part of their own, with no state of its own yet: what they inherit comes from the spliced stream
chunk next;
next.first = m_data.size();
m_chunks.push_back(std::move(next));
m_piecefirst = m_chunks.size() - 1;
m_state.fill(no_call);
m_pushes.clear();
m_work = 0;
}
bool command_stream::empty() const
{
if (false == (m_chunks.empty() || m_data.empty()))
{
return false;
}
for (auto const &piece : m_pieces)
{
if ((piece.stream != this) && (false == piece.stream->empty()))
{
return false;
}
}
return true;
}
std::size_t command_stream::calls() const
{
auto calls{m_calls};
for (auto const &piece : m_pieces)
{
if (piece.stream != this)
{
calls += piece.stream->m_calls;
}
}
return calls;
}
void command_stream::leave_state(std::size_t const Piece, merged_state &State) const
{
auto const &piece{m_pieces[Piece]};
auto const *stream{piece.stream};
// the part of the stream itself left what was kept when it was closed, a spliced stream what it set last
auto const &slots{(stream == this) ? piece.state : std::vector<std::size_t>{stream->m_state.begin(), stream->m_state.end()}};
auto const &pushes{(stream == this) ? piece.pushes : stream->m_pushes};
State.slots.resize(slot_count);
for (std::size_t slot = 0; (slot < slots.size()) && (slot < State.slots.size()); ++slot)
{
if (slots[slot] != no_call)
{
State.slots[slot] = state_ref{stream, slots[slot], Piece};
}
}
for (auto const push : pushes)
{
auto const &info{stream->payload<constants_call>(push).info};
State.pushes.erase(std::remove_if(State.pushes.begin(), State.pushes.end(),
[&](state_ref const &Other) {
auto const &other{Other.stream->payload<constants_call>(Other.call).info};
return (other.offset == info.offset) && (other.size == info.size) && (other.stageFlags == info.stageFlags);
}),
State.pushes.end());
State.pushes.push_back(state_ref{stream, push, Piece});
}
}
std::size_t command_stream::add(std::uint32_t const Type, std::size_t const Size)
@@ -306,12 +464,92 @@ void command_stream::finish()
m_chunks.back().end = m_data.size();
m_chunks.back().suspended = no_call;
// a chunk left without calls
if ((m_chunks.size() > 1) && (m_chunks.back().first == m_chunks.back().end))
if ((m_chunks.size() > 1) && (m_chunks.back().first == m_chunks.back().end) && (m_chunks.size() - 1 > m_piecefirst))
{
m_chunks.pop_back();
m_chunks.back().suspended = no_call;
}
m_finished = true;
if (m_pieces.empty())
{
m_splitchunk = std::min(m_split, m_chunks.size());
return;
}
// the last part of the stream itself
piece last;
last.stream = this;
last.first = m_piecefirst;
last.end = m_chunks.size();
m_pieces.push_back(std::move(last));
// the spliced streams with all their chunks, and the state each part begins with: what the parts before it left
merged_state state;
state.slots.resize(slot_count);
m_order.clear();
for (std::size_t index = 0; index < m_pieces.size(); ++index)
{
auto &piece{m_pieces[index]};
if (piece.stream != this)
{
assert(piece.stream->m_finished);
piece.first = 0;
piece.end = piece.stream->m_chunks.size();
}
piece.inherited.clear();
std::vector<state_ref> refs;
for (auto const &slot : state.slots)
{
if (slot.stream != nullptr)
{
refs.push_back(slot);
}
}
refs.insert(refs.end(), state.pushes.begin(), state.pushes.end());
// in the order they were made: by the part, then by the place in its stream
std::sort(refs.begin(), refs.end(), [](state_ref const &Left, state_ref const &Right) { return (Left.piece != Right.piece) ? (Left.piece < Right.piece) : (Left.call < Right.call); });
refs.erase(std::unique(refs.begin(), refs.end(), [](state_ref const &Left, state_ref const &Right) { return (Left.stream == Right.stream) && (Left.call == Right.call); }), refs.end());
for (auto const &ref : refs)
{
piece.inherited.emplace_back(ref.stream, ref.call);
}
for (auto chunk = piece.first; chunk < piece.end; ++chunk)
{
auto const &source{piece.stream->m_chunks[chunk]};
if ((source.first == source.end) && (source.resumed == no_call))
{
// nothing in it: no command buffer
continue;
}
m_order.emplace_back(index, chunk);
}
leave_state(index, state);
}
if (m_order.empty())
{
// a frame with nothing in it still has a command buffer
m_order.emplace_back(0, m_pieces.front().first);
}
// the split is in a part of the stream itself: the chunks after it begin the second submit, with the parts after that part
m_splitchunk = m_order.size();
if (m_split != no_call)
{
for (std::size_t index = 0; index < m_pieces.size(); ++index)
{
auto const &piece{m_pieces[index]};
if ((piece.stream != this) || (m_split < piece.first) || (m_split >= piece.end))
{
continue;
}
for (std::size_t position = 0; position < m_order.size(); ++position)
{
if ((m_order[position].first > index) || ((m_order[position].first == index) && (m_order[position].second >= m_split)))
{
m_splitchunk = position;
break;
}
}
break;
}
}
}
// the calls
@@ -358,6 +596,12 @@ void command_stream::pipeline_barrier2(VkDependencyInfo const *Dependency)
data.memory = memory;
data.buffers = buffers;
data.images = images;
if (m_compute)
{
compute_queue_barriers(memory != no_data ? reinterpret_cast<VkMemoryBarrier2 *>(m_data.data() + memory) : nullptr, data.memory_count);
compute_queue_barriers(buffers != no_data ? reinterpret_cast<VkBufferMemoryBarrier2 *>(m_data.data() + buffers) : nullptr, data.buffer_count);
compute_queue_barriers(images != no_data ? reinterpret_cast<VkImageMemoryBarrier2 *>(m_data.data() + images) : nullptr, data.image_count);
}
close(call);
}
@@ -1041,6 +1285,27 @@ void command_stream::execute(std::size_t const Call, VkCommandBuffer const Buffe
}
void command_stream::replay(std::size_t const Chunk, VkCommandBuffer const Buffer) const
{
if (false == m_pieces.empty())
{
if (Chunk >= m_order.size())
{
return;
}
auto const &[index, chunk]{m_order[Chunk]};
auto const &piece{m_pieces[index]};
// the state the parts before it left, then the chunk of its stream as it was recorded there
for (auto const &[stream, call] : piece.inherited)
{
stream->execute(call, Buffer);
}
piece.stream->replay_chunk(chunk, Buffer);
return;
}
replay_chunk(Chunk, Buffer);
}
void command_stream::replay_chunk(std::size_t const Chunk, VkCommandBuffer const Buffer) const
{
if (Chunk >= m_chunks.size())
{
@@ -1070,9 +1335,28 @@ void command_stream::replay(std::size_t const Chunk, VkCommandBuffer const Buffe
}
void command_stream::replay(VkCommandBuffer const Buffer) const
{
if (false == m_pieces.empty())
{
// the parts in their order, each a range of the calls of its stream
for (auto const &piece : m_pieces)
{
if (piece.first >= piece.end)
{
continue;
}
auto const &chunks{piece.stream->m_chunks};
piece.stream->replay_calls(chunks[piece.first].first, chunks[piece.end - 1].end, Buffer);
}
return;
}
replay_calls(0, m_data.size(), Buffer);
}
void command_stream::replay_calls(std::size_t const First, std::size_t const End, VkCommandBuffer const Buffer) const
{
// the chunks one after another, the cuts made whole again: each call as it was made
for (auto call = std::size_t{0}; call < m_data.size();)
for (auto call = First; call < End;)
{
auto const &header{*reinterpret_cast<call_header const *>(m_data.data() + call)};
execute(call, Buffer);

View File

@@ -86,30 +86,74 @@ class command_stream
using external_function = void (*)(VkCommandBuffer Buffer, void *Data);
void external(external_function Function, void *Data);
// the calls of Other go to the gpu here, where this stream is now (the stage M2: the passes recorded by other threads at the same
// time): the calls made after this one follow them, with the state they leave (the pipelines, the sets, the constants, the buffers,
// the dynamic state), as if they were made in this stream. outside of a rendering and of a query; Other is a stream without
// streams of its own spliced into it, finished before this one is, and kept until this one is replayed
void splice(command_stream const &Other);
// the stream goes to a queue of the compute family (the async compute of M6): the stages and the accesses of its barriers which such
// a queue doesn't have are left out. the work of the graphics queue they wait for is done before it through the semaphores
void set_compute_queue(bool Compute)
{
m_compute = Compute;
}
// the frame goes to the queue in two submits from here on, the calls after this one in the second, which waits for the work of the
// compute queue (M6). outside of a rendering and of a query; the first one made counts
void split();
// the first chunk of the second submit (chunks() when nothing was split), valid after finish()
std::size_t split_chunk() const
{
return m_splitchunk;
}
// the stream is made anew
void clear();
bool empty() const
{
return m_chunks.empty() || (m_data.empty());
}
bool empty() const;
// the stream is ended: no calls after it
void finish();
// the chunks, valid after finish()
// the chunks, valid after finish(); those of the streams spliced into it, in their places
std::size_t chunks() const
{
return m_chunks.size();
}
// the calls recorded since the start
std::size_t calls() const
{
return m_calls;
return m_pieces.empty() ? m_chunks.size() : m_order.size();
}
// the calls recorded since the start, with those of the streams spliced into it
std::size_t calls() const;
// the chunk recorded into Buffer (begun already, ended by the caller)
void replay(std::size_t Chunk, VkCommandBuffer Buffer) const;
// all of it into one command buffer
void replay(VkCommandBuffer Buffer) const;
private:
// a call of a stream, the stream itself or one spliced into it
using call_ref = std::pair<command_stream const *, std::size_t>;
// a part of the stream as the gpu gets it: chunks of the stream itself or of a stream spliced into it, the state the calls before
// it left (the calls they made last of each kind, in the order they were made) and the state the part leaves with them
struct piece
{
command_stream const *stream{nullptr};
std::size_t first{0}; // chunks of the stream
std::size_t end{0};
std::vector<call_ref> inherited;
// the calls of the part setting the state last, kept when the part is of the stream itself (splice())
std::vector<std::size_t> state;
std::vector<std::size_t> pushes;
};
// the calls setting the state, of all the parts so far: a slot each (state_slot), and the pushes of the constants of each range
// and stages; the part of each, for their order
struct state_ref
{
command_stream const *stream{nullptr};
std::size_t call{0};
std::size_t piece{0};
};
struct merged_state
{
std::vector<state_ref> slots;
std::vector<state_ref> pushes;
};
// the state the calls of the part leave behind, over the state the parts before it left
void leave_state(std::size_t Piece, merged_state &State) const;
// the calls of the state a chunk sets again; the dynamic states have a slot each
enum state_slot : std::uint32_t
{
@@ -176,6 +220,9 @@ class command_stream
void cut();
// Flags: added to those of a begin of a rendering
void execute(std::size_t Call, VkCommandBuffer Buffer, VkRenderingFlags Flags = 0) const;
// a chunk of this stream alone, and the calls between two offsets
void replay_chunk(std::size_t Chunk, VkCommandBuffer Buffer) const;
void replay_calls(std::size_t First, std::size_t End, VkCommandBuffer Buffer) const;
static constexpr std::size_t header_size{8};
std::vector<std::uint8_t> m_data;
@@ -188,6 +235,15 @@ class command_stream
std::uint32_t m_work{0}; // of the chunk under way
std::size_t m_calls{0};
bool m_finished{false};
// the parts of the stream with the streams spliced into it, empty without them; the first chunk of the part of the stream itself
// under way; the chunks in the order of the parts (the part, the chunk of its stream)
std::vector<piece> m_pieces;
std::size_t m_piecefirst{0};
std::vector<std::pair<std::size_t, std::size_t>> m_order;
bool m_compute{false};
// the chunk of the stream itself the split begins, and the first chunk of the second submit
std::size_t m_split{no_call};
std::size_t m_splitchunk{0};
};
} // namespace vk14

View File

@@ -317,14 +317,14 @@ void update_buffer(command_stream *Commands, buffer const &Target, void const *D
// readback
bool readback_ring::create(allocator const &Allocator, VkDeviceSize const Size, std::uint32_t const Frames, std::string const &Name)
bool readback_ring::create(allocator const &Allocator, VkDeviceSize const Size, std::uint32_t const Frames, std::string const &Name, bool const Shared)
{
m_size = Size;
m_slots.clear();
m_slots.resize(Frames);
for (std::uint32_t i = 0; i < Frames; ++i)
{
if (false == m_slots[i].data.create(Allocator, Size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::readback, Name + " " + std::to_string(i)))
if (false == m_slots[i].data.create(Allocator, Size, VK_BUFFER_USAGE_TRANSFER_DST_BIT, memory_kind::readback, Name + " " + std::to_string(i), Shared))
{
return false;
}

View File

@@ -122,7 +122,8 @@ void update_buffer(command_stream *Commands, buffer const &Target, void const *D
class readback_ring
{
public:
bool create(allocator const &Allocator, VkDeviceSize Size, std::uint32_t Frames, std::string const &Name);
// Shared: copied to by the queues of the families the allocator shares between (the async compute of M6)
bool create(allocator const &Allocator, VkDeviceSize Size, std::uint32_t Frames, std::string const &Name, bool Shared = false);
void destroy();
// the copy of Size bytes (at most the size of the ring) of Source from Offset, in the frame of the slot, done by the gpu once
// it reaches Value of the timeline

View File

@@ -37,6 +37,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/vulkan14/vulkan14commands.h"
#include "rendering/vulkan14/vulkan14raytracing.h"
#include "rendering/vulkan14/vulkan14dlss.h"
#include "rendering/vulkan14/vulkan14tasks.h"
#include "utilities/workerpool.h"
#include "model/material.h"
#include "gl/ubo.h"
@@ -49,6 +50,7 @@ http://mozilla.org/MPL/2.0/.
#include <mutex>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace vk14
@@ -92,7 +94,36 @@ struct scene_program : public gfx::shader_program
std::shared_ptr<scene_program> multidraw;
bool multidraw_tried{false};
vertex_kind vertices{vertex_kind::scene};
// the pipelines made, by the key of bind_scene_pipeline(), and the ones the background threads are making (M5). guarded by the
// mutex, as are the variants above made when they're first needed: the walks of the passes and the background threads ask for
// them at once
std::mutex mutex;
std::unordered_map<std::uint64_t, VkPipeline> pipelines;
std::unordered_set<std::uint64_t> pending;
};
// a pipeline of the scene as the list of the pipelines made in a session keeps it, to be made ahead at the start of the next one
// (gfx.vulkan.asyncresources, the stage M5): what bind_scene_pipeline() makes it of, by the names of the shaders
struct scene_pipeline_record
{
// the shaders, by the names and the variants shader_library::get() takes
std::string vertex;
std::string vertex_defines;
std::string fragment;
std::string fragment_defines;
vertex_kind vertices{vertex_kind::scene};
VkPrimitiveTopology topology{VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; // the class
std::uint32_t blending{0};
std::array<VkFormat, 2> colour{VK_FORMAT_UNDEFINED, VK_FORMAT_UNDEFINED};
std::uint32_t colours{0};
VkFormat depth{VK_FORMAT_UNDEFINED};
std::uint32_t samples{1};
std::uint32_t view_mask{0};
std::uint32_t settings{0};
std::string name;
// one line of the list, and the record of it; false: not a line of the list
std::string line() const;
bool parse(std::string const &Line);
};
// what a pass of the scene draws to; the pipelines are made for these
@@ -188,13 +219,18 @@ struct generation_targets
// the ring of a frame some data of the draws goes through, the descriptor of which reads a window of it: the window starts again
// further on when it's full, and the pass set is bound again with the new offset
// the threads of the walkers (the stage M2): as many as the passes they're given in a frame use, within gfx.threads
std::size_t walk_threads();
// the parts of the rings of the draws and of the instances a renderer (the main one, a walker) takes for its slices (vk14::ring_cache)
constexpr VkDeviceSize draw_ring_part{64 * 1024};
constexpr VkDeviceSize instance_ring_part{128 * 1024};
struct window_ring
{
frame_ring ring;
VkDeviceSize window{0}; // the size of the window, bytes
VkDeviceSize stride{0};
VkDeviceSize base{0}; // where the current window starts
bool open{false};
};
// the light clusters (gl::cluster_grid), of the colour pass of each viewport in turn
@@ -487,6 +523,11 @@ struct vulkan14_renderer::context
std::vector<worker_commands> workers;
// the command buffers of the chunks of the frame, in their order
std::vector<VkCommandBuffer> chunks;
// the passes of the frame for the compute queue (the async compute of M6, the sky and the weather), recorded into a command
// buffer of the compute family
vk14::command_stream compute_stream;
VkCommandPool compute_pool{VK_NULL_HANDLE};
VkCommandBuffer compute_buffer{VK_NULL_HANDLE};
// the value of the timeline semaphore the gpu reaches when it's done with the frame; 0: never submitted
std::uint64_t done{0};
// the number of the frame, for the report of a lost device
@@ -521,12 +562,36 @@ struct vulkan14_renderer::context
double submit_time{0.0}; // seconds
// the textures of the files; made after the objects above and gone before them
std::unique_ptr<texture_bank> textures;
// the background threads (the stage M5): the pipelines and the textures made when they're first needed, the pipelines of the last
// session made ahead. stopped before anything they use goes
vk14::background_tasks background;
// the threads of the walkers (the stage M2): the passes of the frame walked at the same time as the main thread walks its own
vk14::background_tasks walks;
// the threads of the helpers of the walks (the stage M3): the groups of a culling walked by several of them, the walk waiting
vk14::background_tasks models;
// the groups they walked since the frame began
std::atomic<std::size_t> helped{0};
// the pass sets of the scene and those of the cullings made anew (the main thread) and those of the cullings written (any walker)
std::mutex passsets_mutex;
// the pipelines of the scene made in the session, written to the list at the end (cache/vulkan14/pipelines.txt), and how many of the
// list of the last session were made ahead and how many weren't made in time for the draw which needed them
std::mutex warm_mutex;
std::vector<vk14::scene_pipeline_record> warm_records;
std::unordered_set<std::string> warm_lines;
std::atomic<std::size_t> warm_made{0};
std::atomic<std::size_t> pipelines_late{0};
// [0]: the main window, then the windows of the extra viewports
std::vector<std::unique_ptr<vk14::swapchain>> windows;
std::vector<frame> frames;
// one semaphore counts the frames done by the gpu; the binary ones are only between the swapchains and the queue
VkSemaphore timeline{VK_NULL_HANDLE};
std::uint64_t timelinevalue{0};
// the async compute (gfx.vulkan.asynccompute, M6), on a device with a compute family of its own: the passes of the sky and the
// weather go to its queue, which signals the frames done by it on a timeline of its own. its work of a frame waits for the
// graphics queue to be done with the frame before (what it writes is read there), the part of the frame after the first pass
// which reads what it makes waits for it
bool async_compute{false};
VkSemaphore compute_timeline{VK_NULL_HANDLE};
// frames recorded since the start
std::uint64_t framecount{0};
// the windows with an image acquired in the frame being made
@@ -547,6 +612,9 @@ struct vulkan14_renderer::context
{
frame *target{nullptr};
bool uploaded{false};
// the value of the timeline of the frame, and whether part of it goes to the compute queue
std::uint64_t value{0};
bool async{false};
std::vector<VkSemaphoreSubmitInfo> waits;
std::vector<VkSemaphoreSubmitInfo> signals;
// the windows presented once the frame is submitted (the render thread)
@@ -575,8 +643,9 @@ struct vulkan14_renderer::context
}
bool create_frames(std::uint32_t const Count, std::string &Reason);
// the streams of Frame recorded into its command buffers; false: no command buffer for them
bool record(frame &Frame, bool Uploads);
// the streams of Frame recorded into its command buffers (Compute: the one of the compute queue too); false: no command buffer for
// them
bool record(frame &Frame, bool Uploads, bool Compute);
// the job done; false: Failure says where it failed
bool execute(render_job &Job, char const *&Failure);
@@ -604,6 +673,18 @@ struct vulkan14_renderer::context
void wait_submitted();
// the parts of the renderer made once the device is there
bool create_core(std::uint32_t const Frames, std::string &Reason);
// the pipelines of the scene (vulkan14scene.cpp, vulkan14_renderer::bind_scene_pipeline()): the fragment shader of a program for
// targets with or without the velocity (Compile: made when it isn't there yet, otherwise null then); the pipeline made now, found
// made already, or made by the background threads (Key: of the program's map); the ones of the list of the last session made
// ahead, the list of this one written
std::shared_ptr<vk14::shader_code const> scene_fragment(vk14::scene_program &Program, bool Single, bool Compile);
VkPipeline make_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single);
bool find_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single,
VkPipeline &Pipeline);
void queue_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode Blend, VkPrimitiveTopology Primitiveclass, std::uint32_t Settings, bool Single,
std::uint64_t Key);
void warm_scene_pipelines();
void save_scene_pipelines();
// the value of the timeline the gpu reached, 0 when it can't be read
std::uint64_t completed() const;
// waits until the gpu reached the value of the timeline; false: the device is lost
@@ -647,6 +728,32 @@ struct vulkan14_renderer::draw_state
bool cullface{true};
bool glare{false};
bool depthmask_off{false}; // the editor overlay: no depth written whatever the blending
// the pipelines and the textures a draw needs first may be made by the background threads, the draw left out till then (the walks
// of the scene, gfx.vulkan.asyncresources); the draws outside of them (the cab, the sky, the particles) wait for what they need
bool async{false};
// the pipeline of the draw before, which the next one takes without asking the program when it's the same
vk14::scene_program const *lastprogram{nullptr};
std::uint64_t lastkey{0};
VkPipeline lastpipeline{VK_NULL_HANDLE};
// the windows of the rings of the draws and of the instances the draws read (where the current one starts), and the parts of the
// rings the renderer places its slices in (vk14::ring_cache; each walker has its own, M2)
struct ring_window
{
VkDeviceSize base{0};
bool open{false};
};
ring_window draws_window;
ring_window instances_window;
vk14::ring_cache draws_cache;
vk14::ring_cache instances_cache;
// the map of a lamp or of headlights the pass draws to (begin_local_shadow_rendering())
vk14::local_shadow_maps::draw_target local_target;
// the pass sets of the scene a walker binds: those of the frame when it was given its pass (the main renderer binds them as they are)
std::array<VkDescriptorSet, vk14::scene_state::set_count> pass_sets{};
// what a walker's draws used, marked as used by the main renderer once the walks are done (the marks aren't made by several
// threads): the banks of the geometry and the textures
std::vector<std::uint32_t> used_banks;
std::vector<texture_handle> used_textures;
// the occlusion queries of the glare of the lamps in the frame: the submodels, by the index of their query
std::vector<TSubModel const *> queries;
std::uint32_t query{~0u}; // the query under way
@@ -658,6 +765,9 @@ struct vulkan14_renderer::draw_state
bool depthskip{true};
// the probe of the cab (the stage A4): the walk draws with the programs of the probe, what they don't draw is left out alike
bool probepass{false};
// the draws recorded by the walk of the groups of a culling are made (draw_indirect_colour()): they take the model blocks recorded
// with them, the state of their submodels isn't read (the passes walked by other threads set it up for theirs, walk_sync)
bool replaying{false};
// the pass sets of a culling, the instance buffer its output, for the indirect draws of its instances; null: the pass sets
VkDescriptorSet const *cull_sets{nullptr};
// the rendering under way, begun again where it was after it was ended for the compute passes of the culling

View File

@@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "rendering/vulkan14/vulkan14cull.h"
#include "rendering/common/walksync.h"
#include "rendering/vulkan14/vulkan14commands.h"
#include "rendering/frustum.h"
@@ -108,6 +109,7 @@ void instance_cull::destroy(VkDevice Device)
m_shared->commands.destroy(Device);
}
m_shared.reset();
m_recordbuffer.reset();
m_counters.reset();
m_output.reset();
m_commands.reset();
@@ -234,7 +236,7 @@ std::vector<descriptor> instance_cull::cull_descriptors(cull_occlusion const *Oc
{
auto const &environment{m_shared->environment};
auto const slice = [](ring_slice const &Slice) { return buffer_descriptor(Slice.buffer, Slice.offset, Slice.size); };
return {buffer_descriptor(m_shared->records->handle(), 0, VK_WHOLE_SIZE),
return {buffer_descriptor(m_recordbuffer->handle(), 0, VK_WHOLE_SIZE),
slice(m_jobs),
slice(m_groups),
slice(m_lods),
@@ -252,7 +254,14 @@ void instance_cull::dispatch(command_stream *Commands, glm::mat4 const &View, cu
}
auto &shared{*m_shared};
auto const &environment{shared.environment};
flush_records();
{
// the places of the records of the pass, the records the passes sent and the buffer they're in, together: another pass may start
// the buffer over in between (walk_sync, the passes walked by several threads at once)
gfx::walk_sync::change change{gfx::walk_sync::parallel()};
refresh_places();
flush_records();
m_recordbuffer = shared.records;
}
m_countersize = (gl::INSTANCE_COUNTER_HEADER + 3 * m_entries) * sizeof(std::uint32_t);
if ((false == reserve(m_counters, std::max(m_countersize, READBACK_SIZE), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "instance counters")) ||

View File

@@ -136,6 +136,8 @@ class instance_cull : public gfx::instance_cull
void read_counters(command_stream *Commands);
std::shared_ptr<shared_data> m_shared;
// the record buffer of the dispatch, which its second phase reads too (another pass may make the shared one anew in between)
std::shared_ptr<buffer> m_recordbuffer;
std::shared_ptr<buffer> m_counters;
std::shared_ptr<buffer> m_output;
std::shared_ptr<buffer> m_commands;

View File

@@ -414,7 +414,7 @@ void vulkan14_renderer::render_cluster_heatmap(std::size_t const Index)
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto &draw{*m_draw};
auto const commands{ctx.commands};
auto const commands{m_commands};
if ((scene.clusters.params.valid == 0) || (false == bool(targets.output)) || (false == bool(targets.depth)))
{
return;

View File

@@ -72,6 +72,11 @@ class device
{
return m_graphics_queue;
}
// the queue of the compute family; the graphics queue where the family is the same
VkQueue compute_queue() const
{
return m_compute_queue;
}
// VK_EXT_memory_budget is on (the allocator keeps to the budget of the heaps)
bool memory_budget_enabled() const
{

View File

@@ -526,6 +526,8 @@ bool vulkan14_renderer::Render()
auto const device{ctx.device.handle()};
auto const slot{static_cast<std::uint32_t>(ctx.framecount % ctx.frames.size())};
auto &frame{ctx.frames[slot]};
// the walkers of a frame which didn't get to its end
join_walks();
Timer::subsystem.gfx_total.start();
@@ -566,6 +568,10 @@ bool vulkan14_renderer::Render()
// what the frames done used last goes
ctx.deletions.collect(std::max(ctx.completed(), frame.done));
VK14_VERIFY(vkResetCommandPool(device, frame.pool, 0));
if (frame.compute_pool != VK_NULL_HANDLE)
{
VK14_VERIFY(vkResetCommandPool(device, frame.compute_pool, 0));
}
for (auto &worker : frame.workers)
{
VK14_VERIFY(vkResetCommandPool(device, worker.pool, 0));
@@ -574,8 +580,11 @@ bool vulkan14_renderer::Render()
ctx.ring.begin_frame(slot);
scene.draws.ring.begin_frame(slot);
scene.instances.ring.begin_frame(slot);
scene.draws.open = false;
scene.instances.open = false;
// the parts of the rings the main renderer places its draws in, and their windows, anew
m_draw->draws_cache.reset(&scene.draws.ring, vk14::draw_ring_part);
m_draw->instances_cache.reset(&scene.instances.ring, vk14::instance_ring_part);
m_draw->draws_window = {};
m_draw->instances_window = {};
// the draws which aren't instanced read the identity at the start of the window from the first draw of the frame on
open_instance_window();
// the occlusion queries of the lamps the slot made the last time
@@ -599,37 +608,46 @@ bool vulkan14_renderer::Render()
// the calls of the frame are kept, recorded into the command buffers once the frame is done
frame.upload_stream.clear();
frame.commands.clear();
frame.compute_stream.clear();
auto const value{ctx.timelinevalue + 1};
ctx.uploads.begin_frame(&frame.upload_stream, value);
ctx.slot = slot;
ctx.commands = &frame.commands;
m_commands = ctx.commands;
if (scene.cull.ready)
{
// the cullings share what they need of the frame
scene.cull.main.begin_frame(slot, value, ctx.completed());
}
scene.lamps.begin_frame(ctx.commands, value);
scene.lamps.begin_frame(m_commands, value);
scene.cabprobe.begin_frame(value);
scene.skyocclusion.begin_frame(value);
if (scene.terrain)
{
// textures of new heightmap terrain layers go to the arrays the terrain shader reads them from
scene.terrain->begin_frame(ctx.commands, value);
scene.terrain->begin_frame(m_commands, value);
scene.terrain->update();
}
// the copies of the light buffer were in the part of the ring of another frame
m_lightheaders.clear();
// the content other threads gave the textures (python)
// the content other threads gave the textures (python), and the textures the background threads made: in the table before the
// walks of the frame, their copies with the uploads of the frame
{
std::vector<vk14::vulkan14_texture *> pending;
std::vector<vk14::vulkan14_texture *> made;
{
std::lock_guard<std::mutex> lock(ctx.pending.mutex);
pending.swap(ctx.pending.textures);
made.swap(ctx.pending.made);
}
for (auto *texture : pending)
{
texture->apply_pending();
}
for (auto *texture : made)
{
texture->publish();
}
}
// the frame generation of dlss: the images of the main window for the frames it makes, where the scene is drawn with dlss. the
@@ -675,7 +693,11 @@ bool vulkan14_renderer::Render()
}
frame.number = ctx.framecount;
ctx.passes.begin_frame(ctx.commands, slot, ctx.framecount);
ctx.passes.begin_frame(m_commands, slot, ctx.framecount);
// the passes of the sky and the weather go to the compute queue once the images of the scene are in their layouts (cleared by the
// graphics queue below the first time) and the noises of the clouds are made (their mipmaps are blitted, which the compute queue
// can't do)
m_asyncframe = ctx.async_compute && scene.images_ready && scene.weather.noise_made && simulation::is_ready && (false == Global.gfx_skiprendering);
// the stand-ins put in place the first time: the empty maps cleared
if (false == scene.images_ready)
@@ -699,11 +721,11 @@ bool vulkan14_renderer::Render()
VkImageSubresourceRange const range{image->aspect(), 0, image->desc().mips, 0, image->desc().layers};
if (image->aspect() == VK_IMAGE_ASPECT_DEPTH_BIT)
{
ctx.commands->clear_depth_stencil_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &range);
m_commands->clear_depth_stencil_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &farthest, 1, &range);
}
else
{
ctx.commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
m_commands->clear_color_image(image->handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
}
uses.push_back(vk14::use_of(*image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access));
}
@@ -716,11 +738,11 @@ bool vulkan14_renderer::Render()
{
uses.push_back(vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access));
}
vk14::barriers(ctx.commands, uses);
vk14::barriers(m_commands, uses);
// no light is assigned to any cluster until the culling pass runs
ctx.commands->fill_buffer(scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0);
m_commands->fill_buffer(scene.clusters.ranges.handle(), 0, VK_WHOLE_SIZE, 0);
// nor does the cab light itself until its probe is drawn
ctx.commands->fill_buffer(scene.cabprobe_bounce.handle(), 0, VK_WHOLE_SIZE, 0);
m_commands->fill_buffer(scene.cabprobe_bounce.handle(), 0, VK_WHOLE_SIZE, 0);
VkMemoryBarrier2 barrier{};
barrier.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2;
barrier.srcStageMask = VK_PIPELINE_STAGE_2_CLEAR_BIT;
@@ -731,7 +753,7 @@ bool vulkan14_renderer::Render()
dependency.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
dependency.memoryBarrierCount = 1;
dependency.pMemoryBarriers = &barrier;
ctx.commands->pipeline_barrier2(&dependency);
m_commands->pipeline_barrier2(&dependency);
// the light of the sky and the exposure from their first state
clear_sky_images();
ctx.passes.end_pass();
@@ -739,7 +761,7 @@ bool vulkan14_renderer::Render()
// the set of the ray tracing of the frame has a structure to read from its first frame on
if (scene.raytracing)
{
scene.raytracing->begin_frame(ctx.commands, slot, value);
scene.raytracing->begin_frame(m_commands, slot, value);
}
// the block of the weather and the scene for the compute passes before the first pass of the scene
m_draw->scene_offset = 0;
@@ -760,12 +782,22 @@ bool vulkan14_renderer::Render()
// directional part of the ambient light. the volumetric clouds of the previous frame tell the sky whether to look overcast
// and dim the sun itself
m_skylight.set_clouds(m_weatherstate.clouds_drawn(), m_weatherstate.cloud_shadows());
if (m_asyncframe)
{
m_commands = &frame.compute_stream;
ctx.passes.begin_async(m_commands);
}
update_sky();
// fog, clouds and stars of the weather, in the light of the sky worked out above
if (false == Global.gfx_skiprendering)
{
update_weather();
}
if (m_asyncframe)
{
ctx.passes.end_async();
m_commands = ctx.commands;
}
// lamps of models and vehicles around the camera and the cab lamp, for the views of the frame to take into their light buffers
if ((false == Global.gfx_skiprendering) && (simulation::Region != nullptr))
{
@@ -800,7 +832,7 @@ bool vulkan14_renderer::Render()
m_nodestateversion = scene::node_state_version;
}
// what the maps of the occlusion of the sky were given, copied to them
scene.skyocclusion.flush(ctx.commands);
scene.skyocclusion.flush(m_commands);
// the sleeper records of tracks out of sight for a while, and the records of the models drawn on their own
Expire_Indirect_Records();
// scenery lamps which get shadows in this frame. their maps hold the scenery as of the version of the instance records
@@ -851,7 +883,9 @@ bool vulkan14_renderer::Render()
}
}
// the windows: the image of their viewport, the user interface over the main one
// the windows: the image of their viewport, the user interface over the main one. what's left of the frame waits for the compute
// queue, where the scene wasn't drawn
split_frame();
bool uidrawn{false};
for (auto *window : ctx.acquired)
{
@@ -882,19 +916,19 @@ bool vulkan14_renderer::Render()
rendering.layerCount = 1;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
ctx.commands->begin_rendering(&rendering);
m_commands->begin_rendering(&rendering);
if (drawscene && (viewport < scene.targets.size()) && scene.targets[viewport].output)
{
present_viewport(viewport);
}
if (ismain)
{
ctx.imgui.record(ctx.commands, window->extent(), window->format(), window->srgb());
ctx.imgui.record(m_commands, window->extent(), window->format(), window->srgb());
uidrawn = true;
}
ctx.commands->end_rendering();
m_commands->end_rendering();
ctx.passes.end_pass();
vk14::barriers(ctx.commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)});
vk14::barriers(m_commands, {window->use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)});
}
if (false == uidrawn)
{
@@ -910,7 +944,7 @@ bool vulkan14_renderer::Render()
scene.screenshot = std::make_shared<vk14::buffer>();
if (scene.screenshot->create(ctx.memory, static_cast<VkDeviceSize>(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::readback, "screenshot"))
{
vk14::barriers(ctx.commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)});
vk14::barriers(m_commands, {vk14::use_of(output, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT)});
VkBufferImageCopy2 region{};
region.sType = VK_STRUCTURE_TYPE_BUFFER_IMAGE_COPY_2;
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
@@ -922,19 +956,24 @@ bool vulkan14_renderer::Render()
copy.dstBuffer = scene.screenshot->handle();
copy.regionCount = 1;
copy.pRegions = &region;
ctx.commands->copy_image_to_buffer2(&copy);
host_read_barrier(ctx.commands);
m_commands->copy_image_to_buffer2(&copy);
host_read_barrier(m_commands);
scene.screenshot_pending = true;
scene.screenshot_value = value;
scene.screenshot_extent = extent;
}
}
// nothing is left to the walkers when the frame is handed over
join_walks();
ctx.passes.end_frame();
vulkan14_renderer::context::render_job job;
job.target = &frame;
job.uploaded = ctx.uploads.end_frame();
job.value = value;
job.async = m_asyncframe;
// the stream of the frame is the render thread's now: nothing records into it any more
ctx.commands = nullptr;
m_commands = nullptr;
m_draw->commands = nullptr;
scene.lamps.end_frame();
if (scene.terrain)
@@ -1043,12 +1082,15 @@ void vulkan14_renderer::render_scene()
ctx.passes.begin_pass("reflection history", {vk14::use_of(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_CLEAR_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)});
VkClearColorValue const black{{0.f, 0.f, 0.f, 0.f}};
VkImageSubresourceRange const range{VK_IMAGE_ASPECT_COLOR_BIT, 0, image.desc().mips, 0, 1};
ctx.commands->clear_color_image(image.handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
vk14::barriers(ctx.commands, {vk14::use_of(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)});
m_commands->clear_color_image(image.handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range);
vk14::barriers(m_commands, {vk14::use_of(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)});
ctx.passes.end_pass();
}
render_viewport(i);
}
// the passes given to the walkers are done before the frame goes on (they were walked at the same time as the viewports)
join_walks();
m_helpedgroups = ctx.helped.exchange(0);
m_current_viewport = (m_viewports.empty() ? nullptr : &m_viewports.front());
}
@@ -1057,7 +1099,7 @@ void vulkan14_renderer::render_scene()
void vulkan14_renderer::start_draws(vk14::target_formats const &Targets, int const Width, int const Height, int const Set, bool const Shadows)
{
auto &draw{*m_draw};
draw.commands = m_context->commands;
draw.commands = m_commands;
draw.targets = &Targets;
draw.extent = {static_cast<std::uint32_t>(Width), static_cast<std::uint32_t>(Height)};
draw.set = static_cast<vk14::scene_state::pass_set_kind>(Set);
@@ -1378,14 +1420,29 @@ void vulkan14_renderer::setup_pass(viewport_config &Viewport, renderpass_config
}
// the colour pass of a viewport (opengl46_renderer::Render_pass(), rendermode::color)
// the frame goes to the queue in two submits from here on, the second waiting for the passes of the sky and the weather on the compute
// queue (the async compute); the first split counts
void vulkan14_renderer::split_frame()
{
if (m_asyncframe && (m_main == nullptr) && (m_commands != nullptr))
{
m_commands->split();
}
}
void vulkan14_renderer::render_viewport(std::size_t const Index)
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &vp{m_viewports[Index]};
auto &targets{scene.targets[Index]};
auto const commands{ctx.commands};
auto const commands{m_commands};
m_current_viewport = &vp;
if (false == vp.main)
{
// the views other than the main one have no sun shadows to draw before what reads the sky and the weather
split_frame();
}
setup_pass(vp, m_renderpass, rendermode::color);
// the temporal anti-aliasing or dlss: the projection of the frame moved by a part of a pixel, the velocities pointed to the last
@@ -1490,6 +1547,9 @@ void vulkan14_renderer::render_viewport(std::size_t const Index)
m_renderpass = m_colorpass;
Timer::subsystem.gfx_shadows.stop();
}
// the sun shadows went on at the same time as the passes of the sky and the weather on the compute queue; the passes from here on
// read what they made (the reflections draw the sky)
split_frame();
if (Global.gfx_envmap_enabled && vp.main && scene.env_map)
{
m_renderpass.draw_stats = {};
@@ -1654,7 +1714,7 @@ void vulkan14_renderer::render_viewport(std::size_t const Index)
}
setup_shadow_color(m_shadowcolor);
model_ubs.future = future;
Render(simulation::Region);
walk_region(simulation::Region, false);
m_indirectcolourpending = false;
// the grass of the terrain, opaque, over the ground drawn
@@ -1667,7 +1727,7 @@ void vulkan14_renderer::render_viewport(std::size_t const Index)
// ...translucent parts
ctx.passes.begin_pass("colour, translucent and cab");
walk_blending(true);
Render_Alpha(simulation::Region);
walk_region(simulation::Region, true);
render_editor_overlay();
// the depth drawn so far, which the particles of the gpu fade in front of
m_particlesoft = (Global.gfx_particles_soft > 0.f) && ((Global.Smoke && Global.gfx_smoke_gpu && (false == simulation::Particles.sequence().empty())) || (Global.gfx_precipitation_gpu && (Global.Overcast > 1.f))) &&
@@ -1740,7 +1800,7 @@ void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 co
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto &draw{*m_draw};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const msaa{targets.samples != VK_SAMPLE_COUNT_1_BIT};
// the depth copied apart from the one the colour pass goes on with, for the contact shadows, the screen space reflections and the
// ambient occlusion in the light of the surfaces
@@ -1813,7 +1873,7 @@ void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 co
Render_cab(vehicle, vehicle->InteriorLightLevel, false);
}
model_ubs.future = Future;
Render(simulation::Region);
walk_region(simulation::Region, false);
m_renderpass.draw_stats = colourstats;
commands->set_depth_bias_enable(VK_FALSE);
draw.depthpass = false;
@@ -1966,7 +2026,7 @@ void vulkan14_renderer::render_ray_scene(viewport_config &Viewport)
auto const buildstart{std::chrono::steady_clock::now()};
ctx.passes.begin_pass("ray tracing scene");
scene.raytracing->build(ctx.commands, ctx.slot, ctx.uploads.current_value(), m_framestamp, m_rtdraws, ctx.images);
scene.raytracing->build(m_commands, ctx.slot, ctx.uploads.current_value(), m_framestamp, m_rtdraws, ctx.images);
ctx.passes.end_pass();
auto const buildend{std::chrono::steady_clock::now()};
m_rtwalkms += (std::chrono::duration<double, std::milli>(buildstart - walkstart).count() - m_rtwalkms) * 0.05;
@@ -1986,12 +2046,11 @@ void vulkan14_renderer::gather_ray_draw(gfx::geometry_handle const &Geometry)
{
return;
}
gfx::indirect_source indirect;
if (false == m_geometry.indirect_source(Geometry, indirect))
vk14::chunk_source chunk;
if (false == chunk_of(Geometry, chunk))
{
return;
}
auto const &chunk{static_cast<vk14::vulkan14_geometrybank *>(m_banks[Geometry.bank - 1])->last()};
if (((chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST) && (chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP) && (chunk.topology != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN)) || (chunk.vertex_address == 0))
{
return;
@@ -2060,7 +2119,7 @@ void vulkan14_renderer::store_ssr_history(std::size_t const Index, vk14::image &
{
return;
}
auto const commands{ctx.commands};
auto const commands{m_commands};
auto &image{*targets.ssr_history};
auto const mips{image.desc().mips};
auto const width{static_cast<std::int32_t>(image.desc().extent.width)};
@@ -2165,7 +2224,7 @@ void vulkan14_renderer::begin_colour_rendering(std::size_t const Index, bool con
rendering.colorAttachmentCount = targets.formats.colours;
rendering.pColorAttachments = colours.data();
rendering.pDepthAttachment = &depth;
ctx.commands->begin_rendering(&rendering);
m_commands->begin_rendering(&rendering);
remember_rendering(rendering, colour_target_uses(targets));
}
@@ -2181,7 +2240,7 @@ void vulkan14_renderer::walk_editor_cursor()
return;
}
auto &targets{scene.targets.front()};
auto const commands{ctx.commands};
auto const commands{m_commands};
glm::vec2 mousepos = Global.cursor_pos;
// cursor coordinates are flipped compared to opengl, whose rows the images keep
mousepos.y = Global.window_size.y - mousepos.y;
@@ -2238,7 +2297,7 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport)
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const commands{ctx.commands};
auto const commands{m_commands};
setup_pass(Viewport, m_renderpass, rendermode::shadows);
auto csmstageboundaries{gfx::shadow_fit::fixed_splits()};
// the cascades fitted to the depth of the view (gfx.shadow.sdsm) go between the distances of its pixels, each map covering the
@@ -2247,6 +2306,12 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport)
auto const fitted{shadow_fit(Viewport, csmstageboundaries, fitareas)};
scene_ubs.shadow_fit = glm::vec4{fitted ? 1.f : 0.f, 0.f, 0.f, 0.f};
m_shadowfitstate.splits = csmstageboundaries;
// the cascades walked by the walkers (gfx.vulkan.parallelpasses), each with a culling of its own made beforehand
auto const walkers{walks_wanted()};
if (walkers)
{
ensure_cull();
}
static char const *const parts[] = {"sun shadows 1", "sun shadows 2", "sun shadows 3"};
for (std::size_t idx = 0; idx < m_shadowpass.size(); ++idx)
{
@@ -2261,39 +2326,56 @@ void vulkan14_renderer::render_shadows(viewport_config &Viewport)
scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION);
auto const csmstagezfar{(csmstageboundaries[idx + 1]) + (m_shadowpass.size() - idx)};
scene_ubs.cascade_end[idx] = csmstagezfar * csmstagezfar;
VkClearValue clear{};
clear.depthStencil = {0.f, 0};
auto depth{attachment_info(scene.shadow_layers[idx], VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, clear)};
VkRenderingInfo rendering{};
rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}};
rendering.layerCount = 1;
rendering.pDepthAttachment = &depth;
commands->begin_rendering(&rendering);
remember_rendering(rendering, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)});
start_draws(scene.shadow_formats, static_cast<int>(scene.shadow_size), static_cast<int>(scene.shadow_size), vk14::scene_state::set_plain, true);
m_shadowcascade = static_cast<int>(idx);
walk_blending(false);
upload_scene_block();
if (m_shadowcolor != colors::white)
if (walkers)
{
Render(simulation::Region);
// render cab only in the closest csm stage
if ((idx == 0) && !FreeFlyModeFlag && Global.render_cab && simulation::Train)
{
Render_cab(simulation::Train->Dynamic(), 0.0f, false);
Render_cab(simulation::Train->Dynamic(), 0.0f, true);
}
fork_walk([idx](vulkan14_renderer &Walker) { Walker.walk_cascade(idx); });
}
else
{
walk_cascade(idx);
}
m_shadowcascade = -1;
commands->end_rendering();
end_draws();
ctx.passes.end_pass();
}
vk14::barriers(commands, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)});
}
// the walk of a cascade of the sun shadows set up by render_shadows(): by the main renderer, or by a walker given the state of the frame
void vulkan14_renderer::walk_cascade(std::size_t const Index)
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const commands{m_commands};
// the matrices of the camera of the cascade (the walker's own, made from the pass it was given)
setup_matrices();
VkClearValue clear{};
clear.depthStencil = {0.f, 0};
auto depth{attachment_info(scene.shadow_layers[Index], VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, true, clear)};
VkRenderingInfo rendering{};
rendering.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
rendering.renderArea = {{0, 0}, {scene.shadow_size, scene.shadow_size}};
rendering.layerCount = 1;
rendering.pDepthAttachment = &depth;
commands->begin_rendering(&rendering);
remember_rendering(rendering, {vk14::use_of(*scene.shadow_map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)});
start_draws(scene.shadow_formats, static_cast<int>(scene.shadow_size), static_cast<int>(scene.shadow_size), vk14::scene_state::set_plain, true);
m_shadowcascade = static_cast<int>(Index);
walk_blending(false);
upload_scene_block();
if (m_shadowcolor != colors::white)
{
walk_region(simulation::Region, false);
// render cab only in the closest csm stage
if ((Index == 0) && !FreeFlyModeFlag && Global.render_cab && simulation::Train)
{
Render_cab(simulation::Train->Dynamic(), 0.0f, false);
Render_cab(simulation::Train->Dynamic(), 0.0f, true);
}
}
m_shadowcascade = -1;
commands->end_rendering();
end_draws();
}
// the environment map (the sky and the clouds around the camera), a face of it per frame (opengl46_renderer::Render_reflections())
bool vulkan14_renderer::render_reflections(viewport_config &Viewport)
{
@@ -2322,14 +2404,14 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport)
for (; m_environmentupdateface <= lastface; ++m_environmentupdateface)
{
// the depth of the faces is the same image: the clear of a face waits for the draws of the one before
vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)});
vk14::barriers(m_commands, {vk14::use_of(*scene.env_depth, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)});
m_environmentcubetextureface = m_environmentupdateface;
render_reflection_face(Viewport);
}
if (m_environmentupdateface >= 6)
{
// the mipmaps, each level from the one before it
auto const commands{ctx.commands};
auto const commands{m_commands};
auto &image{*scene.env_map};
auto const mips{image.desc().mips};
auto const size{static_cast<std::int32_t>(image.desc().extent.width)};
@@ -2390,7 +2472,7 @@ bool vulkan14_renderer::render_reflections(viewport_config &Viewport)
}
else
{
vk14::barriers(ctx.commands, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)});
vk14::barriers(m_commands, {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, sampled_stage, sampled_access)});
}
return true;
}
@@ -2399,7 +2481,7 @@ void vulkan14_renderer::render_reflection_face(viewport_config &Viewport)
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const size{scene.env_map->desc().extent.width};
setup_pass(Viewport, m_renderpass, rendermode::reflections);
@@ -2825,7 +2907,7 @@ bool vulkan14_renderer::setup_particles_depth(std::size_t const Index)
return false;
}
}
auto const commands{ctx.commands};
auto const commands{m_commands};
suspend_rendering();
vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT),
vk14::use_of(*targets.scene_depth, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT)});
@@ -3108,7 +3190,7 @@ void vulkan14_renderer::post_pass(char const *Fragment, vk14::image &Target, std
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const commands{ctx.commands};
auto const commands{m_commands};
VkExtent2D const extent{Target.desc().extent.width, Target.desc().extent.height};
std::vector<vk14::image_use> uses{vk14::use_of(Target, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, colour_stage, colour_access)};
for (auto *input : Inputs)
@@ -3204,7 +3286,7 @@ void vulkan14_renderer::render_dlss(std::size_t const Index)
inputs.jitter = m_taa.jitter();
inputs.reset = m_dlssreset || (false == m_taa.history_valid());
inputs.frame_time = static_cast<float>(Timer::GetDeltaRenderTime() * 1000.0);
ctx.dlss.evaluate(*ctx.commands, inputs, ctx.deletions, ctx.uploads.current_value());
ctx.dlss.evaluate(*m_commands, inputs, ctx.deletions, ctx.uploads.current_value());
m_dlssreset = false;
ctx.passes.end_pass();
}
@@ -3381,14 +3463,14 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
rendering.layerCount = 1;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
ctx.commands->begin_rendering(&rendering);
m_commands->begin_rendering(&rendering);
if (drawn)
{
present_viewport(Index);
}
// the user interface in each of them (kept for the next one but the last)
ctx.imgui.record(ctx.commands, extent, window.format(), window.srgb(), false == last);
ctx.commands->end_rendering();
ctx.imgui.record(m_commands, extent, window.format(), window.srgb(), false == last);
m_commands->end_rendering();
ctx.passes.end_pass();
}
std::vector<vk14::image_use> presents{window.use(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE)};
@@ -3396,7 +3478,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
{
presents.push_back(window.generated_use(i, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE));
}
vk14::barriers(ctx.commands, presents);
vk14::barriers(m_commands, presents);
return;
}
@@ -3410,13 +3492,13 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
rendering.layerCount = 1;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
ctx.commands->begin_rendering(&rendering);
m_commands->begin_rendering(&rendering);
present_viewport(Index, &Target);
if (Ui)
{
ctx.imgui.record(ctx.commands, extent, Target.desc().format, false);
ctx.imgui.record(m_commands, extent, Target.desc().format, false);
}
ctx.commands->end_rendering();
m_commands->end_rendering();
ctx.passes.end_pass();
};
drawto(*generation.hudless, false, "frame generation hudless");
@@ -3455,7 +3537,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
// its history is of no use after a frame without it (minimized, not drawn, without the images)
inputs.reset = (false == m_taa.history_valid()) || (m_generationframe + 1 != ctx.framecount);
m_generationframe = ctx.framecount;
ctx.dlss.generate(*ctx.commands, inputs, ctx.deletions, ctx.uploads.current_value());
ctx.dlss.generate(*m_commands, inputs, ctx.deletions, ctx.uploads.current_value());
ctx.passes.end_pass();
// the frames to the images of the window: the ones generated first, in the order they are shown, then the one drawn
@@ -3487,7 +3569,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
blit.regionCount = 1;
blit.pRegions = &region;
blit.filter = VK_FILTER_NEAREST;
ctx.commands->blit_image2(&blit);
m_commands->blit_image2(&blit);
return;
}
VkImageCopy2 region{};
@@ -3503,7 +3585,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
copy.dstImageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
copy.regionCount = 1;
copy.pRegions = &region;
ctx.commands->copy_image2(&copy);
m_commands->copy_image2(&copy);
};
for (std::uint32_t i = 0; i < window.generated_count(); ++i)
{
@@ -3516,7 +3598,7 @@ void vulkan14_renderer::render_generation(std::size_t const Index)
{
presents.push_back(window.generated_use(i, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_PIPELINE_STAGE_2_NONE, VK_ACCESS_2_NONE));
}
vk14::barriers(ctx.commands, presents);
vk14::barriers(m_commands, presents);
}
// the post-processing of a viewport: from the colour of the scene to its output, which the window shows (the part of
@@ -3526,7 +3608,7 @@ void vulkan14_renderer::render_post(std::size_t const Index, bool const Motionbl
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto const commands{ctx.commands};
auto const commands{m_commands};
// the occlusion goes into the image before the blur, which smears both alike. the targets are there only when it was on as they
// were made. in the light of the surfaces it's in the image already
@@ -3624,7 +3706,7 @@ void vulkan14_renderer::present_viewport(std::size_t const Index, vk14::image co
auto &scene{ctx.scene};
auto &targets{scene.targets[Index]};
auto const &window{*ctx.windows[m_viewports[Index].window]};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const format{Target != nullptr ? Target->desc().format : window.format()};
auto const srgb{Target != nullptr ? false : window.srgb()};
VkExtent2D const extent{Target != nullptr ? VkExtent2D{Target->desc().extent.width, Target->desc().extent.height} : window.extent()};
@@ -3858,7 +3940,7 @@ void vulkan14_renderer::update_lights(light_array &Lights, bool const Headlights
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &clusters{scene.clusters};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const lightsize{offsetof(gl::light_data, lights) + sizeof(gl::light_element) * m_lightcount};
auto const lights{ctx.ring.allocate(sizeof(gl::light_data), ctx.device.caps().limits.minStorageBufferOffsetAlignment)};
if (false == bool(lights))
@@ -3945,7 +4027,7 @@ void vulkan14_renderer::update_clusters(glm::ivec2 const &Targetsize)
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &clusters{scene.clusters};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const projection{OpenGLMatrices.data(GL_PROJECTION)};
auto const znear{m_renderpass.znear};
auto const zfar{m_renderpass.zfar};
@@ -4010,7 +4092,7 @@ void vulkan14_renderer::render_picking(rendermode const Mode)
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto &readback{Mode == rendermode::pickcontrols ? scene.pick_controls : scene.pick_scenery};
if (readback.pending || m_viewports.empty())
{

View File

@@ -237,6 +237,7 @@ bool vulkan14_geometrybank::source(gfx::geometry_handle const &Geometry, chunk_s
{
return false;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (m_layoutdirty)
{
build();
@@ -281,15 +282,15 @@ bool vulkan14_geometrybank::source(gfx::geometry_handle const &Geometry, chunk_s
bool vulkan14_geometrybank::indirect_source_(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source)
{
m_last = chunk_source{};
if (false == source(Geometry, m_last))
chunk_source chunk;
if (false == source(Geometry, chunk))
{
return false;
}
Source.count = m_last.count;
Source.first = m_last.first;
Source.basevertex = m_last.basevertex;
Source.indexed = (m_last.indices != VK_NULL_HANDLE);
Source.count = chunk.count;
Source.first = chunk.first;
Source.basevertex = chunk.basevertex;
Source.indexed = (chunk.indices != VK_NULL_HANDLE);
Source.type = m_chunks[Geometry.chunk - 1].type;
return true;
}

View File

@@ -20,6 +20,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/vulkan14/vulkan14memory.h"
#include <memory>
#include <mutex>
namespace vk14
{
@@ -55,12 +56,9 @@ class vulkan14_geometrybank : public gfx::geometry_bank
~vulkan14_geometrybank() override;
// the chunk with its data on the gpu: uploaded now when it changed since the last time. false: nothing to draw.
// gfx::geometrybank_manager::indirect_source() calls it too (which marks the bank as used), and keeps its answer in last()
// gfx::geometrybank_manager::indirect_source() calls it too (which marks the bank as used). any thread walking the passes (M2);
// what changes the chunks is made while none of them draws (gfx::walk_sync)
bool source(gfx::geometry_handle const &Geometry, chunk_source &Source);
chunk_source const &last() const
{
return m_last;
}
// the bytes on the gpu, for the debug panel
VkDeviceSize gpu_bytes() const
{
@@ -101,7 +99,8 @@ class vulkan14_geometrybank : public gfx::geometry_bank
bool m_layoutdirty{true}; // the buffers have to be made anew
bool m_chunksdirty{false}; // some chunks have to be copied
VkDeviceSize m_bytes{0};
chunk_source m_last;
// the buffers made or updated by one of the threads drawing from the bank at a time
std::mutex m_mutex;
};
} // namespace vk14

View File

@@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/.
#include "stdafx.h"
#include "rendering/vulkan14/vulkan14context.h"
#include "rendering/vulkan14/vulkan14commands.h"
#include "rendering/common/walksync.h"
#include "model/AnimModel.h"
#include "model/Model3d.h"
#include "utilities/Globals.h"
@@ -103,22 +104,29 @@ vk14::instance_cull &vulkan14_renderer::pass_cull()
// the gpu culling runs along the first color pass of the main viewport of a frame, and the passes of the cascades of the sun
// shadows and of the faces of the environment map drawn for it, each with a culling of its own (opengl46_renderer::walk_region_begin())
// the cullings made the first time they're wanted, by the main renderer (before it gives the passes to the walkers)
void vulkan14_renderer::ensure_cull()
{
auto &cull{m_context->scene.cull};
static bool tried{false};
if ((m_main == nullptr) && (Global.gfx_gpucull != 0) && (false == cull.ready) && (false == tried))
{
tried = true;
if (false == init_cull())
{
ErrorLog("vulkan: the gpu culling of the instances can't be used, the cpu culls them (gfx.gpucull)");
destroy_cull();
}
}
}
void vulkan14_renderer::walk_region_begin()
{
vk14::instance_cull *passcull{nullptr};
auto &cull{m_context->scene.cull};
if ((Global.gfx_gpucull != 0) && (m_current_viewport != nullptr) && (m_current_viewport->main) && m_draw->rendering)
{
static bool tried{false};
if ((false == cull.ready) && (false == tried))
{
tried = true;
if (false == init_cull())
{
ErrorLog("vulkan: the gpu culling of the instances can't be used, the cpu culls them (gfx.gpucull)");
destroy_cull();
}
}
ensure_cull();
if (cull.ready)
{
if ((m_renderpass.draw_mode == rendermode::color) && (m_instancecullframe != m_framestamp))
@@ -169,7 +177,7 @@ bool vulkan14_renderer::walk_instances_begin()
{
// the view of the pass is in place here, the one Render_Instanced() takes
suspend_rendering();
pass_cull().dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW));
pass_cull().dispatch(m_commands, OpenGLMatrices.data(GL_MODELVIEW));
resume_rendering();
// what the cpu drew of the sleepers and the models drawn on their own
Compare_Pass_Cull();
@@ -219,7 +227,7 @@ void vulkan14_renderer::suspend_rendering()
{
return;
}
m_context->commands->end_rendering();
m_commands->end_rendering();
draw.rendering = false;
}
@@ -231,9 +239,17 @@ void vulkan14_renderer::resume_rendering()
{
return;
}
auto const commands{m_context->commands};
// what the rendering stored before is loaded again
vk14::barriers(commands, resume.uses);
auto const commands{m_commands};
// what the rendering stored before is loaded again. a walker leaves the states of the images the main thread tracks as they were:
// the rendering left them as it found them
if (m_main != nullptr)
{
vk14::barriers_again(commands, resume.uses);
}
else
{
vk14::barriers(commands, resume.uses);
}
commands->begin_rendering(&resume.info);
// the dynamic state starts anew with the rendering, the state of the walk goes on
auto const set{draw.set};
@@ -263,6 +279,8 @@ void vulkan14_renderer::resume_rendering()
vk14::scene_program *vulkan14_renderer::multidraw_program(vk14::scene_program &Program)
{
// made by one of the threads walking the passes (M2)
std::lock_guard<std::mutex> lock(Program.mutex);
if (Program.multidraw_tried)
{
return Program.multidraw.get();
@@ -309,12 +327,11 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con
return false;
}
// the buffers of the bank of the chunks, with their data on the gpu (the chunks of a multi draw are of one bank)
gfx::indirect_source source;
if (false == m_geometry.indirect_source(geometry, source))
vk14::chunk_source chunk;
if (false == chunk_of(geometry, chunk))
{
return false;
}
auto const &chunk{static_cast<vk14::vulkan14_geometrybank *>(m_banks[geometry.bank - 1])->last()};
if (false == bind_scene_pipeline(Program, *draw.targets, Blend, static_cast<int>(chunk.topology)))
{
return false;
@@ -334,21 +351,26 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con
else
{
auto &draws{scene.draws};
auto &window{draw.draws_window};
auto const size{static_cast<VkDeviceSize>(Count) * draws.stride};
auto const slice{size <= draws.window ? draws.ring.allocate(size, draws.stride) : vk14::ring_slice{}};
if (slice)
auto slice{(window.open && (size <= draws.window)) ? draw.draws_cache.allocate_before(size, draws.stride, window.base + draws.window) : vk14::ring_slice{}};
if ((false == bool(slice)) && (size <= draws.window))
{
if ((false == draws.open) || (slice.offset + size > draws.base + draws.window))
slice = draw.draws_cache.allocate(size, draws.stride);
if (slice)
{
draws.base = slice.offset;
draws.open = true;
window.base = slice.offset;
window.open = true;
draw.set_dirty = true;
}
}
if (slice)
{
for (std::uint32_t i = 0; i < Count; ++i)
{
std::memcpy(slice.data + i * draws.stride, &m_indirectdraws[First + i].ubs, sizeof(gl::model_ubs));
}
block = static_cast<std::uint32_t>((slice.offset - draws.base) / draws.stride);
block = static_cast<std::uint32_t>((slice.offset - window.base) / draws.stride);
}
}
// the next draw of the walk pushes a block of its own
@@ -416,6 +438,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_colour(vk14:
indirect_counts counts;
auto const count{m_indirectdraws.size()};
auto const longest{static_cast<std::size_t>(scene.draws.window / scene.draws.stride)};
draw.replaying = true;
for (std::size_t index = 0; index < count;)
{
auto const blending{m_indirectdraws[index].blending};
@@ -450,6 +473,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_colour(vk14:
walk_blending(false);
}
}
draw.replaying = false;
if (count > 0)
{
// the model block the draws made one by one leave behind
@@ -463,7 +487,7 @@ vulkan14_renderer::indirect_counts vulkan14_renderer::draw_indirect_depth(vk14::
{
auto &draw{*m_draw};
auto &scene{m_context->scene};
auto const commands{m_context->commands};
auto const commands{m_commands};
// as in draw_indirect_colour(): the seed the draws were recorded with
TSubModel::iVariantSeed = 0;
indirect_counts counts;
@@ -601,7 +625,7 @@ void vulkan14_renderer::record_indirect_draws(vk14::instance_cull &Cull, bool co
{
Order_Indirect_Draws();
}
Cull.fill_commands(m_context->commands, m_indirectcommands);
Cull.fill_commands(m_commands, m_indirectcommands);
m_walkcounts.ordertime += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - orderstart).count();
}
@@ -614,7 +638,7 @@ void vulkan14_renderer::render_instances_indirect()
auto &scene{ctx.scene};
auto &cull{scene.cull.main};
auto &draw{*m_draw};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const viewport{static_cast<std::size_t>(m_current_viewport - m_viewports.data())};
auto *targets{viewport < scene.targets.size() ? &scene.targets[viewport] : nullptr};
auto const projection{OpenGLMatrices.data(GL_PROJECTION)};
@@ -718,7 +742,7 @@ void vulkan14_renderer::render_instances_indirect_pass()
auto &cull{pass_cull()};
auto &cascades{m_context->scene.cull.cascades};
suspend_rendering();
cull.dispatch(m_context->commands, OpenGLMatrices.data(GL_MODELVIEW));
cull.dispatch(m_commands, OpenGLMatrices.data(GL_MODELVIEW));
record_indirect_draws(cull, (&cull >= cascades.data()) && (&cull < cascades.data() + cascades.size()));
resume_rendering();
@@ -807,7 +831,7 @@ void vulkan14_renderer::measure_shadow_fit(std::size_t const Viewport)
return;
}
auto &targets{scene.targets[Viewport]};
auto const commands{ctx.commands};
auto const commands{m_commands};
vk14::barriers(commands, {vk14::use_of(*targets.depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT)});
gfx::shadow_fit::view const view{m_colorpass.pass_camera.position(), m_colorpass.pass_camera.projection(), glm::mat3{m_colorpass.pass_camera.modelview()},
gfx::shadow_fit::light_rotation(m_sunlight.direction), gfx::shadow_fit::fixed_splits().back()};

View File

@@ -28,19 +28,31 @@ VkPipelineStageFlags2 const sampled_stage{VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BI
} // namespace
// the rendering of the map the maps set up last (begin(), begin_lamp(), begin_headlight()), and the pass of the walk into it
// the rendering of the map the maps set up last (begin(), begin_lamp(), begin_headlight()), and the pass of the walk into it: the map
// made ready for it (the states of the images are tracked by the main renderer), then begun (by the main renderer, or by a walker given
// the target, begin_local_shadow_target())
void vulkan14_renderer::begin_local_shadow_rendering()
{
m_draw->local_target = m_context->scene.lamps.target();
auto const &target{m_draw->local_target};
if ((target.map == nullptr) || (target.view == VK_NULL_HANDLE))
{
return;
}
vk14::barriers(m_commands, {vk14::use_of(*target.map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)});
}
void vulkan14_renderer::begin_local_shadow_target()
{
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto const &target{scene.lamps.target()};
auto const commands{ctx.commands};
auto const &target{m_draw->local_target};
auto const commands{m_commands};
if ((target.map == nullptr) || (target.view == VK_NULL_HANDLE))
{
return;
}
auto const use{vk14::use_of(*target.map, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, depth_stage, depth_access)};
vk14::barriers(commands, {use});
VkRenderingAttachmentInfo depth{};
depth.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
depth.imageView = target.view;
@@ -70,7 +82,7 @@ void vulkan14_renderer::end_local_shadow_rendering()
{
if (m_draw->rendering)
{
m_context->commands->end_rendering();
m_commands->end_rendering();
}
end_draws();
}
@@ -89,7 +101,7 @@ void vulkan14_renderer::ready_lamp_maps()
}
if (false == uses.empty())
{
vk14::barriers(m_context->commands, uses);
vk14::barriers(m_commands, uses);
}
}
@@ -112,6 +124,7 @@ void vulkan14_renderer::render_local_shadows()
OpenGLMatrices.load_matrix(glm::mat4{glm::translate(glm::dmat4{1.0}, -glm::dvec3{lamps.lamp()}) * glm::inverse(vehicle->mMatrix)});
lamps.begin();
begin_local_shadow_rendering();
begin_local_shadow_target();
m_cubeshadows = true;
Render_cab(vehicle, 0.0f, false);
m_cubeshadows = false;
@@ -198,13 +211,46 @@ void vulkan14_renderer::render_lamp_shadow(std::size_t const Slot, bool const Ve
camera.update_frustum();
OpenGLMatrices.mode(GL_MODELVIEW);
OpenGLMatrices.load_matrix(glm::mat4{1.f});
lamps.begin_lamp(Slot, Vehicles, m_instancerecordsversion, m_framestamp);
begin_local_shadow_rendering();
// the vehicles by the lamp; or the scenery, with its instances culled on the gpu, each scenery part drawn in a frame with a culling
// of its own
std::vector<TDynamicObject *> vehicles;
vk14::instance_cull *cull{nullptr};
if (Vehicles)
{
vehicles = lamps.slot_vehicles();
}
else
{
cull = light_shadow_cull(m_context->scene.cull.lamps, m_lampscenerydrawn++, Global.gfx_gpucull_shadows_lamps);
}
auto const *owner{static_cast<TAnimModel const *>(slot.owner)};
if (walks_wanted())
{
// walked by a walker (gfx.vulkan.parallelpasses), its draws counted once it's done
fork_walk([target = m_draw->local_target, Vehicles, vehicles, Cab, owner, cull](vulkan14_renderer &Walker) {
Walker.m_draw->local_target = target;
Walker.walk_lamp(Vehicles, vehicles, Cab, owner, cull);
}, walk_kind::lamp);
lamps.end_lamp(0);
}
else
{
walk_lamp(Vehicles, vehicles, Cab, owner, cull);
lamps.end_lamp(static_cast<std::uint32_t>(m_renderpass.draw_stats.drawcalls));
}
m_renderpass = m_colorpass;
}
// the walk of the map of a lamp set up by render_lamp_shadow()
void vulkan14_renderer::walk_lamp(bool const Vehicles, std::vector<TDynamicObject *> const &Dynamics, TDynamicObject *Cab, TAnimModel const *Owner, vk14::instance_cull *Cull)
{
begin_local_shadow_target();
m_cubeshadows = true;
if (Vehicles)
{
for (auto *vehicle : lamps.slot_vehicles())
for (auto *vehicle : Dynamics)
{
Render(vehicle);
}
@@ -216,18 +262,15 @@ void vulkan14_renderer::render_lamp_shadow(std::size_t const Slot, bool const Ve
else
{
m_cubescenery = true;
m_cubeskipinstance = static_cast<TAnimModel const *>(slot.owner);
// the instances culled on the gpu, each scenery part drawn in a frame with a culling of its own
m_lightshadowcull = light_shadow_cull(m_context->scene.cull.lamps, m_lampscenerydrawn++, Global.gfx_gpucull_shadows_lamps);
Render(simulation::Region);
m_cubeskipinstance = Owner;
m_lightshadowcull = Cull;
walk_region(simulation::Region, false);
m_lightshadowcull = nullptr;
m_cubeskipinstance = nullptr;
m_cubescenery = false;
}
m_cubeshadows = false;
end_local_shadow_rendering();
lamps.end_lamp(static_cast<std::uint32_t>(m_renderpass.draw_stats.drawcalls));
m_renderpass = m_colorpass;
}
void vulkan14_renderer::render_headlight_shadows()
@@ -311,23 +354,46 @@ void vulkan14_renderer::render_headlight_shadow(std::size_t const Slot, light_ar
setup_matrices();
auto const colorprojection{scene_ubs.projection};
scene_ubs.projection = projection;
lamps.begin_headlight(Slot, projector, rotation, projection, Light.direction, m_framestamp);
begin_local_shadow_rendering();
m_shadowskipvehicle = Light.owner;
// the instances culled on the gpu, each slot with a culling of its own
m_lightshadowcull = light_shadow_cull(m_context->scene.cull.headlights, Slot, Global.gfx_gpucull_shadows_headlights);
Render(simulation::Region);
// the instances culled on the gpu, each slot with a culling of its own; the occupied cab, within the reach
auto *cull{light_shadow_cull(m_context->scene.cull.headlights, Slot, Global.gfx_gpucull_shadows_headlights)};
auto *cab{((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius())) ? Cab : nullptr};
auto const *owner{Light.owner};
if (walks_wanted())
{
// walked by a walker (gfx.vulkan.parallelpasses), its draws counted once it's done
fork_walk([target = m_draw->local_target, owner, cab, cull](vulkan14_renderer &Walker) {
Walker.m_draw->local_target = target;
Walker.walk_headlight(owner, cab, cull);
}, walk_kind::headlight);
lamps.end_headlight(0);
}
else
{
walk_headlight(owner, cab, cull);
lamps.end_headlight(static_cast<std::uint32_t>(m_renderpass.draw_stats.drawcalls));
}
scene_ubs.projection = colorprojection;
m_renderpass = m_colorpass;
}
// the walk of the map of a set of headlights set up by render_headlight_shadow()
void vulkan14_renderer::walk_headlight(TDynamicObject const *Owner, TDynamicObject *Cab, vk14::instance_cull *Cull)
{
// the matrices of the projector (the walker's own, made from the pass it was given)
setup_matrices();
begin_local_shadow_target();
m_shadowskipvehicle = Owner;
m_lightshadowcull = Cull;
walk_region(simulation::Region, false);
m_lightshadowcull = nullptr;
if ((Cab != nullptr) && (Cab != Light.owner) && (glm::length(Cab->GetPosition() - projector) <= reach + Cab->radius()))
if (Cab != nullptr)
{
Render_cab(Cab, 0.0f, false);
}
m_shadowskipvehicle = nullptr;
end_local_shadow_rendering();
lamps.end_headlight(static_cast<std::uint32_t>(m_renderpass.draw_stats.drawcalls));
scene_ubs.projection = colorprojection;
m_renderpass = m_colorpass;
}
vk14::instance_cull *vulkan14_renderer::light_shadow_cull(std::vector<std::unique_ptr<vk14::instance_cull>> &Culls, std::size_t const Index, bool const Enabled)

View File

@@ -77,6 +77,24 @@ void allocator::destroy()
m_device = VK_NULL_HANDLE;
}
void allocator::share_between(std::uint32_t const First, std::uint32_t const Second)
{
m_shared = {First, Second};
}
VkSharingMode allocator::sharing(bool const Shared, std::uint32_t &Count, std::uint32_t const *&Families) const
{
Count = 0;
Families = nullptr;
if ((false == Shared) || (m_shared[0] == VK_QUEUE_FAMILY_IGNORED) || (m_shared[1] == VK_QUEUE_FAMILY_IGNORED) || (m_shared[0] == m_shared[1]))
{
return VK_SHARING_MODE_EXCLUSIVE;
}
Count = 2;
Families = m_shared.data();
return VK_SHARING_MODE_CONCURRENT;
}
void allocator::budget(VkDeviceSize &Usage, VkDeviceSize &Budget) const
{
Usage = 0;
@@ -124,7 +142,7 @@ buffer::~buffer()
destroy();
}
bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name)
bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name, bool const Shared)
{
destroy();
@@ -132,7 +150,7 @@ bool buffer::create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsage
info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
info.size = std::max<VkDeviceSize>(Size, 4);
info.usage = Usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.sharingMode = Allocator.sharing(Shared, info.queueFamilyIndexCount, info.pQueueFamilyIndices);
VmaAllocationCreateInfo allocation{};
allocation.usage = VMA_MEMORY_USAGE_AUTO;
@@ -249,7 +267,7 @@ bool image::create(allocator const &Allocator, image_desc const &Desc, std::stri
info.samples = Desc.samples;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = Desc.usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.sharingMode = Allocator.sharing(Desc.shared, info.queueFamilyIndexCount, info.pQueueFamilyIndices);
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VmaAllocationCreateInfo allocation{};
@@ -353,38 +371,50 @@ void transition(command_stream *Commands, image &Image, VkImageLayout Layout, Vk
void deletion_queue::push(std::uint64_t Value, std::function<void()> &&Deleter)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_entries.emplace_back(Value, std::move(Deleter));
}
void deletion_queue::collect(std::uint64_t Completed)
{
// the values only grow, so the queue is in their order
while ((false == m_entries.empty()) && (m_entries.front().first <= Completed))
// the values only grow, so the queue is in their order (an object let go by another thread while the frame is made takes the
// value of the frame too). the deleters run outside of the lock
std::vector<std::function<void()>> deleters;
{
std::lock_guard<std::mutex> lock(m_mutex);
while ((false == m_entries.empty()) && (m_entries.front().first <= Completed))
{
deleters.emplace_back(std::move(m_entries.front().second));
m_entries.pop_front();
}
}
for (auto &deleter : deleters)
{
auto deleter{std::move(m_entries.front().second)};
m_entries.pop_front();
deleter();
}
}
void deletion_queue::flush()
{
while (false == m_entries.empty())
std::deque<std::pair<std::uint64_t, std::function<void()>>> entries;
{
auto deleter{std::move(m_entries.front().second)};
m_entries.pop_front();
deleter();
std::lock_guard<std::mutex> lock(m_mutex);
entries.swap(m_entries);
}
for (auto &entry : entries)
{
entry.second();
}
}
// frame ring
bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail)
bool frame_ring::create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail, bool const Shared)
{
m_sizeperframe = SizePerFrame;
m_base = 0;
m_used = 0;
return m_buffer.create(Allocator, SizePerFrame * Frames + Tail, Usage, memory_kind::upload, Name) && (m_buffer.mapped() != nullptr);
return m_buffer.create(Allocator, SizePerFrame * Frames + Tail, Usage, memory_kind::upload, Name, Shared) && (m_buffer.mapped() != nullptr);
}
void frame_ring::destroy()
@@ -396,12 +426,14 @@ void frame_ring::destroy()
void frame_ring::begin_frame(std::uint32_t Frame)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_base = m_sizeperframe * Frame;
m_used = 0;
}
ring_slice frame_ring::allocate(VkDeviceSize Size, VkDeviceSize Alignment)
{
std::lock_guard<std::mutex> lock(m_mutex);
auto const alignment{std::max<VkDeviceSize>(Alignment, 4)};
auto const offset{(m_used + alignment - 1) / alignment * alignment};
if ((m_buffer.mapped() == nullptr) || (offset + Size > m_sizeperframe))
@@ -419,7 +451,73 @@ ring_slice frame_ring::allocate(VkDeviceSize Size, VkDeviceSize Alignment)
void frame_ring::flush() const
{
std::lock_guard<std::mutex> lock(m_mutex);
m_buffer.flush(m_base, m_used);
}
void ring_cache::reset(frame_ring *const Ring, VkDeviceSize const Part)
{
m_ring = Ring;
m_partsize = Part;
m_part = ring_slice{};
m_used = 0;
}
VkDeviceSize ring_cache::next(VkDeviceSize const Alignment) const
{
auto const alignment{std::max<VkDeviceSize>(Alignment, 4)};
return (m_part.offset + m_used + alignment - 1) / alignment * alignment;
}
ring_slice ring_cache::allocate_before(VkDeviceSize const Size, VkDeviceSize const Alignment, VkDeviceSize const Limit)
{
if (false == bool(m_part))
{
return {};
}
auto const offset{next(Alignment)};
if ((offset + Size > m_part.offset + m_part.size) || (offset + Size > Limit))
{
return {};
}
m_used = offset + Size - m_part.offset;
ring_slice slice;
slice.buffer = m_part.buffer;
slice.offset = offset;
slice.size = Size;
slice.data = m_part.data + (offset - m_part.offset);
return slice;
}
ring_slice ring_cache::allocate(VkDeviceSize const Size, VkDeviceSize const Alignment)
{
if (auto const slice{allocate_before(Size, Alignment, std::numeric_limits<VkDeviceSize>::max())})
{
return slice;
}
if (m_ring == nullptr)
{
return {};
}
// the parts begin aligned for any of the slices
auto const partalignment{std::max<VkDeviceSize>(Alignment, 256)};
if (Size > m_partsize / 2)
{
// a large one gets a part of its own, the part held is kept for the small ones
return m_ring->allocate(Size, partalignment);
}
m_part = m_ring->allocate(m_partsize, partalignment);
m_used = 0;
if (false == bool(m_part))
{
// the ring is nearly full: the rest of it in a part of the size of the slice
m_part = m_ring->allocate(Size, partalignment);
if (false == bool(m_part))
{
return {};
}
}
return allocate_before(Size, Alignment, std::numeric_limits<VkDeviceSize>::max());
}
} // namespace vk14

View File

@@ -16,6 +16,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/vulkan14/vulkan14common.h"
#include <deque>
#include <mutex>
#include <functional>
#include <vk_mem_alloc.h>
@@ -47,10 +48,16 @@ class allocator
}
// device memory used and the budget of the heaps which are local to the device, bytes
void budget(VkDeviceSize &Usage, VkDeviceSize &Budget) const;
// the buffers and images made shared from now on are used by the queues of both families without transfers of their ownership
// (VK_SHARING_MODE_CONCURRENT, the async compute of M6); nothing changes when the families are the same
void share_between(std::uint32_t First, std::uint32_t Second);
// the sharing of a buffer or an image made shared: the mode, and the families of the concurrent one (two of them)
VkSharingMode sharing(bool Shared, std::uint32_t &Count, std::uint32_t const *&Families) const;
private:
VmaAllocator m_allocator{VK_NULL_HANDLE};
VkDevice m_device{VK_NULL_HANDLE};
std::array<std::uint32_t, 2> m_shared{VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED};
std::uint32_t m_heapcount{0};
std::vector<bool> m_deviceheaps;
};
@@ -73,7 +80,8 @@ class buffer
buffer &operator=(buffer &&Other) noexcept;
~buffer();
bool create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name = {});
// Shared: used by the queues of the families the allocator shares between (allocator::share_between())
bool create(allocator const &Allocator, VkDeviceSize Size, VkBufferUsageFlags Usage, memory_kind Kind, std::string const &Name = {}, bool Shared = false);
void destroy();
VkBuffer handle() const
@@ -130,6 +138,8 @@ struct image_desc
VkImageCreateFlags flags{0};
// the channels the default view reads, e.g. a single channel texture seen as rrr1
VkComponentMapping components{VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY};
// used by the queues of the families the allocator shares between (allocator::share_between())
bool shared{false};
};
class image
@@ -183,6 +193,8 @@ class image
void transition(command_stream *Commands, image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage, VkAccessFlags2 Access);
// objects destroyed once the gpu is done with the frame which used them last
// pushed to from any thread (the walks of the passes and the background threads make and let go of objects too), collected by the
// thread of the renderer
class deletion_queue
{
public:
@@ -194,10 +206,12 @@ class deletion_queue
void flush();
std::size_t size() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_entries.size();
}
private:
mutable std::mutex m_mutex;
std::deque<std::pair<std::uint64_t, std::function<void()>>> m_entries;
};
@@ -220,21 +234,23 @@ class frame_ring
{
public:
// Tail: bytes past the part of the last frame, so a descriptor of a fixed range can be given an offset anywhere in a part
bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail = 0);
bool create(allocator const &Allocator, VkDeviceSize SizePerFrame, std::uint32_t Frames, VkBufferUsageFlags Usage, std::string const &Name, VkDeviceSize Tail = 0, bool Shared = false);
void destroy();
// the part of the frame Frame is used from its start again
void begin_frame(std::uint32_t Frame);
// Size bytes aligned to Alignment; empty when the part of the frame is full
// Size bytes aligned to Alignment; empty when the part of the frame is full. any thread (the walks of the passes, M2)
ring_slice allocate(VkDeviceSize Size, VkDeviceSize Alignment);
// makes the writes of the frame seen by the gpu, where the memory isn't coherent
void flush() const;
VkDeviceSize used() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_used;
}
// the offset in the buffer the next allocation aligned to Alignment would get
VkDeviceSize next_offset(VkDeviceSize Alignment) const
{
std::lock_guard<std::mutex> lock(m_mutex);
auto const alignment{std::max<VkDeviceSize>(Alignment, 4)};
return m_base + (m_used + alignment - 1) / alignment * alignment;
}
@@ -248,10 +264,35 @@ class frame_ring
}
private:
mutable std::mutex m_mutex;
buffer m_buffer;
VkDeviceSize m_sizeperframe{0};
VkDeviceSize m_base{0};
VkDeviceSize m_used{0};
};
// a part of a frame ring taken by a thread for the many small slices it places there (the blocks of the draws, the matrices of the
// instances), so the threads walking the passes (M2) don't take the lock of the ring for each of them. the slices follow one another
// in the part; one which doesn't fit in what's left of it takes a new part (or a part of its own size when it's larger than a part)
class ring_cache
{
public:
// Ring: where the parts come from (null: none), Part: their size, bytes. the part held is let go
void reset(frame_ring *Ring, VkDeviceSize Part);
// Size bytes aligned to Alignment (at most 256); empty when the ring is full
ring_slice allocate(VkDeviceSize Size, VkDeviceSize Alignment);
// the same, only when the slice fits in the part held and ends before Limit (an offset in the buffer); empty, with nothing taken,
// otherwise
ring_slice allocate_before(VkDeviceSize Size, VkDeviceSize Alignment, VkDeviceSize Limit);
private:
// where the next slice aligned to Alignment would begin in the part held, as an offset in the buffer
VkDeviceSize next(VkDeviceSize Alignment) const;
frame_ring *m_ring{nullptr};
VkDeviceSize m_partsize{0};
ring_slice m_part;
VkDeviceSize m_used{0}; // of the part held
};
} // namespace vk14

View File

@@ -79,6 +79,21 @@ void barriers(command_stream *Commands, std::vector<image_use> const &Uses)
Commands->pipeline_barrier2(&dependency);
}
void barriers_again(command_stream *Commands, std::vector<image_use> const &Uses)
{
// what barriers() makes of a use following the same use
std::vector<image_use> uses;
std::vector<image_state> states(Uses.size());
for (std::size_t i = 0; i < Uses.size(); ++i)
{
auto use{Uses[i]};
states[i] = image_state{use.layout, use.stage, use.access};
use.state = &states[i];
uses.push_back(use);
}
barriers(Commands, uses);
}
pass_list::~pass_list()
{
destroy();
@@ -95,6 +110,8 @@ bool pass_list::create(device const &Device, std::uint32_t Frames, std::string &
vkGetPhysicalDeviceQueueFamilyProperties(Device.physical(), &count, families.data());
auto const bits{(Device.families().graphics < count) ? families[Device.families().graphics].timestampValidBits : 64u};
m_mask = (bits >= 64 ? ~0ull : ((1ull << bits) - 1));
auto const asyncbits{(Device.families().compute < count) ? families[Device.families().compute].timestampValidBits : 64u};
m_asyncmask = (asyncbits >= 64 ? ~0ull : ((1ull << asyncbits) - 1));
m_frames.resize(Frames);
for (std::uint32_t i = 0; i < Frames; ++i)
@@ -112,6 +129,15 @@ bool pass_list::create(device const &Device, std::uint32_t Frames, std::string &
// the queries are reset from the host before their first use (hostQueryReset)
vkResetQueryPool(m_device, m_frames[i].pool, 0, info.queryCount);
name_object(m_device, VK_OBJECT_TYPE_QUERY_POOL, handle_value(m_frames[i].pool), "pass times " + std::to_string(i));
info.queryCount = max_async * 2;
if (!VK14_VERIFY(vkCreateQueryPool(m_device, &info, nullptr, &m_frames[i].async_pool)))
{
m_frames[i].async_pool = VK_NULL_HANDLE;
Reason = "no query pool for the times of the passes";
return false;
}
vkResetQueryPool(m_device, m_frames[i].async_pool, 0, info.queryCount);
name_object(m_device, VK_OBJECT_TYPE_QUERY_POOL, handle_value(m_frames[i].async_pool), "async pass times " + std::to_string(i));
}
return true;
}
@@ -124,6 +150,10 @@ void pass_list::destroy()
{
vkDestroyQueryPool(m_device, frame.pool, nullptr);
}
if (frame.async_pool != VK_NULL_HANDLE)
{
vkDestroyQueryPool(m_device, frame.async_pool, nullptr);
}
}
m_frames.clear();
m_device = VK_NULL_HANDLE;
@@ -132,6 +162,8 @@ void pass_list::destroy()
void pass_list::begin_frame(command_stream *Commands, std::uint32_t Frame, std::uint64_t Number)
{
m_commands = Commands;
m_graphics = nullptr;
m_async = false;
m_frame = Frame;
m_open = -1;
auto &frame{m_frames[Frame]};
@@ -140,6 +172,14 @@ void pass_list::begin_frame(command_stream *Commands, std::uint32_t Frame, std::
frame.number = Number;
frame.recorded = true;
Commands->reset_query_pool(frame.pool, 0, max_passes * 2);
// the gpu is done with the frame which used the slot before: the queries of the compute queue are reset from here, the queue may
// write them before the graphics queue gets to a reset of its own
if (frame.async_used > 0)
{
vkResetQueryPool(m_device, frame.async_pool, 0, max_async * 2);
}
frame.async_names.clear();
frame.async_used = 0;
}
void pass_list::begin_pass(char const *Name, std::vector<image_use> const &Uses)
@@ -151,6 +191,21 @@ void pass_list::begin_pass(char const *Name, std::vector<image_use> const &Uses)
barriers(m_commands, Uses);
begin_label(m_commands, Name);
auto &frame{m_frames[m_frame]};
if (m_async)
{
if ((frame.async_used < max_async) && (m_asyncmask != 0))
{
m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT, frame.async_pool, frame.async_used * 2);
frame.async_names.emplace_back(Name);
m_open = static_cast<int>(frame.async_used);
++frame.async_used;
}
else
{
m_open = static_cast<int>(max_passes);
}
return;
}
// (no timestamps on a queue without valid bits; the device is chosen with them, this is for safety)
if ((frame.used < max_passes) && (m_mask != 0))
{
@@ -173,14 +228,39 @@ void pass_list::end_pass()
}
if (m_open < static_cast<int>(max_passes))
{
m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_frames[m_frame].pool, static_cast<std::uint32_t>(m_open) * 2 + 1);
m_commands->write_timestamp2(VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT, m_async ? m_frames[m_frame].async_pool : m_frames[m_frame].pool, static_cast<std::uint32_t>(m_open) * 2 + 1);
}
end_label(m_commands);
m_open = -1;
}
void pass_list::begin_async(command_stream *Commands)
{
end_pass();
if (m_async)
{
return;
}
m_graphics = m_commands;
m_commands = Commands;
m_async = true;
}
void pass_list::end_async()
{
end_pass();
if (false == m_async)
{
return;
}
m_commands = m_graphics;
m_graphics = nullptr;
m_async = false;
}
void pass_list::end_frame()
{
end_async();
end_pass();
m_commands = nullptr;
}
@@ -193,14 +273,34 @@ void pass_list::collect(std::uint32_t Frame)
return;
}
frame.recorded = false;
// a running average, so the numbers can be read
auto const blend = [](double &Average, double const Value) { Average = (Average <= 0.0 ? Value : Average * 0.9 + Value * 0.1); };
// the passes of the compute queue, apart
if (frame.async_used > 0)
{
std::vector<std::uint64_t> stamps(frame.async_used * 2);
if (vkGetQueryPoolResults(m_device, frame.async_pool, 0, frame.async_used * 2, stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT) == VK_SUCCESS)
{
for (std::uint32_t i = 0; i < frame.async_used; ++i)
{
auto const ticks{((stamps[i * 2 + 1] & m_asyncmask) - (stamps[i * 2] & m_asyncmask)) & m_asyncmask};
auto const name{frame.async_names[i] + " (async)"};
auto lookup{std::find_if(m_asynctimes.begin(), m_asynctimes.end(), [&](auto const &Entry) { return Entry.first == name; })};
if (lookup == m_asynctimes.end())
{
m_asynctimes.emplace_back(name, 0.0);
lookup = std::prev(m_asynctimes.end());
}
blend(lookup->second, static_cast<double>(ticks) * m_period / 1000000.0);
}
}
}
std::vector<std::uint64_t> stamps(frame.used * 2);
auto const result{vkGetQueryPoolResults(m_device, frame.pool, 0, frame.used * 2, stamps.size() * sizeof(std::uint64_t), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT)};
if (result != VK_SUCCESS)
{
return;
}
// a running average, so the numbers can be read
auto const blend = [](double &Average, double const Value) { Average = (Average <= 0.0 ? Value : Average * 0.9 + Value * 0.1); };
double total{0.0};
for (std::uint32_t i = 0; i < frame.used; ++i)
{
@@ -246,6 +346,11 @@ std::string pass_list::info() const
}
std::snprintf(buffer, sizeof(buffer), " %-14s %7.3f ms\n", "total", m_framems);
text += buffer;
for (auto const &[name, ms] : m_asynctimes)
{
std::snprintf(buffer, sizeof(buffer), " %-14s %7.3f ms\n", name.c_str(), ms);
text += buffer;
}
return text;
}

View File

@@ -40,6 +40,9 @@ image_use use_of(image &Image, VkImageLayout Layout, VkPipelineStageFlags2 Stage
// the barriers which bring the images from their states to the uses, the states updated
void barriers(command_stream *Commands, std::vector<image_use> const &Uses);
// the barriers between two uses of the images in the states of the uses, made one after another (a rendering ended and begun again
// by a walker, M2), without the states, which the main thread tracks
void barriers_again(command_stream *Commands, std::vector<image_use> const &Uses);
class pass_list
{
@@ -57,6 +60,10 @@ class pass_list
// a pass: the barriers of its images, its label and the timestamp of its start
void begin_pass(char const *Name, std::vector<image_use> const &Uses = {});
void end_pass();
// the passes from here on go to Commands, for the compute queue (the async compute of M6), until end_async(); timed with queries of
// their own, as their times overlap those of the passes of the graphics queue: they're shown apart, not in the total
void begin_async(command_stream *Commands);
void end_async();
void end_frame();
// the gpu times of the frame Frame, once the gpu is done with it (after the wait for its value of the timeline)
void collect(std::uint32_t Frame);
@@ -76,20 +83,29 @@ class pass_list
VkQueryPool pool{VK_NULL_HANDLE};
std::vector<std::string> names;
std::uint32_t used{0};
// those of the compute queue, reset from the host
VkQueryPool async_pool{VK_NULL_HANDLE};
std::vector<std::string> async_names;
std::uint32_t async_used{0};
std::uint64_t number{0};
bool recorded{false};
};
static constexpr std::uint32_t max_passes{32};
static constexpr std::uint32_t max_async{8};
VkDevice m_device{VK_NULL_HANDLE};
float m_period{1.f}; // ns per tick
std::uint64_t m_mask{~0ull}; // the valid bits of the timestamps
std::uint64_t m_asyncmask{~0ull}; // those of the compute queue
std::vector<frame_queries> m_frames;
command_stream *m_commands{nullptr};
command_stream *m_graphics{nullptr}; // the stream of the graphics queue, while the passes go to the compute one
bool m_async{false};
std::uint32_t m_frame{0};
int m_open{-1};
// averaged times of the parts, ms
std::vector<std::pair<std::string, double>> m_times;
std::vector<std::pair<std::string, double>> m_asynctimes; // of the compute queue
double m_framems{0.0};
};

View File

@@ -113,6 +113,21 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s
}
}
// the async compute: a family of its own for the passes of the sky and the weather, the frames it's done with counted apart
async_compute = Global.gfx_vulkan_asynccompute && device.families().dedicated_compute && (device.families().compute != vk14::queue_families::invalid) && (device.compute_queue() != VK_NULL_HANDLE);
if (async_compute)
{
if (!VK14_VERIFY(vkCreateSemaphore(handle, &semaphoreinfo, nullptr, &compute_timeline)))
{
compute_timeline = VK_NULL_HANDLE;
async_compute = false;
}
else
{
instance.set_object_name(handle, VK_OBJECT_TYPE_SEMAPHORE, reinterpret_cast<std::uint64_t>(compute_timeline), "compute timeline");
}
}
frames.resize(Count);
for (std::uint32_t i = 0; i < Count; ++i)
{
@@ -136,6 +151,22 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s
return false;
}
instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast<std::uint64_t>(frames[i].uploads), "uploads " + std::to_string(i));
frames[i].compute_stream.set_compute_queue(true);
if (async_compute)
{
poolinfo.queueFamilyIndex = device.families().compute;
bufferinfo.commandPool = VK_NULL_HANDLE;
if (VK14_VERIFY(vkCreateCommandPool(handle, &poolinfo, nullptr, &frames[i].compute_pool)))
{
bufferinfo.commandPool = frames[i].compute_pool;
}
if ((bufferinfo.commandPool == VK_NULL_HANDLE) || !VK14_VERIFY(vkAllocateCommandBuffers(handle, &bufferinfo, &frames[i].compute_buffer)))
{
Reason = "no command buffer of the compute queue";
return false;
}
instance.set_object_name(handle, VK_OBJECT_TYPE_COMMAND_BUFFER, reinterpret_cast<std::uint64_t>(frames[i].compute_buffer), "async compute " + std::to_string(i));
}
}
if (Global.gfx_vulkan_renderthread)
{
@@ -144,7 +175,7 @@ bool vulkan14_renderer::context::create_frames(std::uint32_t const Count, std::s
return true;
}
bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads)
bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads, bool const Compute)
{
auto const handle{device.handle()};
auto const start{std::chrono::steady_clock::now()};
@@ -159,6 +190,13 @@ bool vulkan14_renderer::context::record(frame &Frame, bool const Uploads)
Frame.upload_stream.replay(Frame.uploads);
VK14_VERIFY(vkEndCommandBuffer(Frame.uploads));
}
if (Compute)
{
Frame.compute_stream.finish();
VK14_VERIFY(vkBeginCommandBuffer(Frame.compute_buffer, &begin));
Frame.compute_stream.replay(Frame.compute_buffer);
VK14_VERIFY(vkEndCommandBuffer(Frame.compute_buffer));
}
// the chunks split evenly between the workers, each with a command pool of its own (a pool is used by one thread at a time);
// which worker records a chunk doesn't matter: each chunk has a command buffer of its own, they go to the queue in their order
auto const chunks{Frame.commands.chunks()};
@@ -233,11 +271,41 @@ bool vulkan14_renderer::context::submit(render_job &Job, char const *&Failure)
{
main->latency_marker(Job.present_id, VK_LATENCY_MARKER_RENDERSUBMIT_START_NV);
}
if (false == record(frame, Job.uploaded))
if (false == record(frame, Job.uploaded, Job.async))
{
Failure = "recording a frame";
return false;
}
if (Job.async)
{
// the passes of the compute queue, once the graphics queue is done with the frame before, which read what they write
VkSemaphoreSubmitInfo wait{};
wait.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
wait.semaphore = timeline;
wait.value = Job.value - 1;
wait.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
VkSemaphoreSubmitInfo signal{};
signal.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
signal.semaphore = compute_timeline;
signal.value = Job.value;
signal.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
VkCommandBufferSubmitInfo buffer{};
buffer.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
buffer.commandBuffer = frame.compute_buffer;
VkSubmitInfo2 compute{};
compute.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
compute.waitSemaphoreInfoCount = (Job.value > 1 ? 1 : 0);
compute.pWaitSemaphoreInfos = &wait;
compute.commandBufferInfoCount = 1;
compute.pCommandBufferInfos = &buffer;
compute.signalSemaphoreInfoCount = 1;
compute.pSignalSemaphoreInfos = &signal;
if (!VK14_VERIFY(vkQueueSubmit2(device.compute_queue(), 1, &compute, VK_NULL_HANDLE)))
{
Failure = "submitting the async compute of a frame";
return false;
}
}
// the uploads first, in the same submit: their barriers make them done before the frame uses what they copied
std::vector<VkCommandBufferSubmitInfo> commands;
if (Job.uploaded)
@@ -255,22 +323,43 @@ bool vulkan14_renderer::context::submit(render_job &Job, char const *&Failure)
info.commandBuffer = chunk;
commands.push_back(info);
}
VkSubmitInfo2 submit{};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
VkLatencySubmissionPresentIdNV latency{};
latency.sType = VK_STRUCTURE_TYPE_LATENCY_SUBMISSION_PRESENT_ID_NV;
latency.presentID = Job.present_id;
// with the async compute in two submits: the part before the split goes on at the same time as the compute queue, the rest waits
// for it. the waits for the images of the windows and the signals go with the rest, which draws them
std::array<VkSubmitInfo2, 2> submits{};
std::uint32_t count{0};
auto first{commands.size()};
std::vector<VkSemaphoreSubmitInfo> waits;
if (Job.async)
{
first = (Job.uploaded ? 1 : 0) + std::min(frame.commands.split_chunk(), frame.chunks.size());
auto &before{submits[count++]};
before.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
before.commandBufferInfoCount = static_cast<std::uint32_t>(first);
before.pCommandBufferInfos = commands.data();
VkSemaphoreSubmitInfo compute{};
compute.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO;
compute.semaphore = compute_timeline;
compute.value = Job.value;
compute.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
waits.push_back(compute);
}
waits.insert(waits.end(), Job.waits.begin(), Job.waits.end());
auto &submit{submits[count++]};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2;
if (lowlatency)
{
submit.pNext = &latency;
}
submit.waitSemaphoreInfoCount = static_cast<std::uint32_t>(Job.waits.size());
submit.pWaitSemaphoreInfos = Job.waits.data();
submit.commandBufferInfoCount = static_cast<std::uint32_t>(commands.size());
submit.pCommandBufferInfos = commands.data();
submit.waitSemaphoreInfoCount = static_cast<std::uint32_t>(waits.size());
submit.pWaitSemaphoreInfos = waits.data();
submit.commandBufferInfoCount = static_cast<std::uint32_t>(commands.size() - (Job.async ? first : 0));
submit.pCommandBufferInfos = commands.data() + (Job.async ? first : 0);
submit.signalSemaphoreInfoCount = static_cast<std::uint32_t>(Job.signals.size());
submit.pSignalSemaphoreInfos = Job.signals.data();
if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), 1, &submit, VK_NULL_HANDLE)))
if (!VK14_VERIFY(vkQueueSubmit2(device.graphics_queue(), count, submits.data(), VK_NULL_HANDLE)))
{
Failure = "submitting a frame";
return false;
@@ -505,14 +594,23 @@ bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::st
{
return false;
}
if (async_compute)
{
// what the passes of the compute queue read and write is shared by the two families
memory.share_between(device.families().graphics, device.families().compute);
}
uploads.init(memory, deletions, device.physical());
// the textures of the background threads filled by the host, where the device can do it to the layout they're sampled in
uploads.enable_host_copy(device.caps().host_image_copy && device.caps().host_copy_sampled);
if (false == sampling.create(device, static_cast<float>(Global.AnisotropicFiltering), Reason, lod_bias))
{
return false;
}
auto const &limits{device.caps().limits};
(void)limits;
if (false == ring.create(memory, ring_size, Frames, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "frame ring", ring_tail))
// the blocks of the frame are read by the passes of the compute queue too (the async compute)
auto const ringusage{VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT};
if (false == ring.create(memory, ring_size, Frames, ringusage, "frame ring", ring_tail, true))
{
Reason = "no ring for the data of the frames";
return false;
@@ -529,6 +627,7 @@ bool vulkan14_renderer::context::create_core(std::uint32_t const Frames, std::st
resources.images = &images;
resources.thread = std::this_thread::get_id();
resources.pending = &pending;
resources.background = (Global.gfx_vulkan_asyncresources ? &background : nullptr);
resources.raytracing = device.raytracing_enabled();
textures = std::make_unique<texture_bank>([this]() -> texture_source * { return new vk14::vulkan14_texture(resources); });
return imgui.create(resources, shaders, pipelines, ring, Reason);
@@ -562,6 +661,8 @@ bool vulkan14_renderer::context::wait(std::uint64_t const Value) const
void vulkan14_renderer::context::destroy()
{
auto const handle{device.handle()};
// the background threads use what goes below
background.stop();
// the frame given to the render thread goes to the queue first
stop_render_thread();
if (handle != VK_NULL_HANDLE)
@@ -575,7 +676,7 @@ void vulkan14_renderer::context::destroy()
// the textures give their images to the deletion queue, which goes now that the gpu is idle
textures.reset();
uploads.end_frame();
uploads = vk14::uploader{};
uploads.reset();
images.clear();
deletions.flush();
// the features of dlss released by the deletions above, ngx goes before the device
@@ -597,6 +698,10 @@ void vulkan14_renderer::context::destroy()
{
vkDestroyCommandPool(handle, worker.pool, nullptr);
}
if (frame.compute_pool != VK_NULL_HANDLE)
{
vkDestroyCommandPool(handle, frame.compute_pool, nullptr);
}
}
if (timeline != VK_NULL_HANDLE)
{
@@ -606,11 +711,17 @@ void vulkan14_renderer::context::destroy()
{
vkDestroySemaphore(handle, reflex, nullptr);
}
if (compute_timeline != VK_NULL_HANDLE)
{
vkDestroySemaphore(handle, compute_timeline, nullptr);
}
}
memory.destroy();
frames.clear();
timeline = VK_NULL_HANDLE;
reflex = VK_NULL_HANDLE;
compute_timeline = VK_NULL_HANDLE;
async_compute = false;
device.destroy();
instance.destroy();
}
@@ -719,7 +830,8 @@ void vulkan14_renderer::start_dlss()
bool vulkan14_renderer::Init(GLFWwindow *Window)
{
m_context = std::make_unique<context>();
m_contextstore = std::make_unique<context>();
m_context = m_contextstore.get();
auto &ctx{*m_context};
std::string reason;
@@ -772,7 +884,8 @@ bool vulkan14_renderer::Init(GLFWwindow *Window)
if (false == ok)
{
vk14::log_error("the renderer can't start: " + reason);
m_context.reset();
m_contextstore.reset();
m_context = nullptr;
return false;
}
@@ -780,7 +893,8 @@ bool vulkan14_renderer::Init(GLFWwindow *Window)
{
vk14::log_error("the renderer can't start: " + reason);
destroy_scene();
m_context.reset();
m_contextstore.reset();
m_context = nullptr;
return false;
}
init_frame_data();
@@ -855,6 +969,15 @@ bool vulkan14_renderer::AddViewport(global_settings::extraviewport_config const
void vulkan14_renderer::Shutdown()
{
if (m_main != nullptr)
{
// a walker has nothing of its own to let go of
return;
}
if (m_context)
{
join_walks();
}
if (m_context && (m_context->device.handle() != VK_NULL_HANDLE))
{
// the end of the renderer, not a frame: everything on the gpu has to be done, the frame of the render thread with it
@@ -870,8 +993,11 @@ void vulkan14_renderer::Shutdown()
}
}
destroy_scene();
m_walkers.clear();
m_walkstreams.clear();
m_draw.reset();
m_context.reset();
m_contextstore.reset();
m_context = nullptr;
}
void vulkan14_renderer::report_device_lost(char const *Where)
@@ -1049,7 +1175,7 @@ void vulkan14_renderer::update_info()
ctx.memory.budget(usage, budget);
m_infostats = "device memory: " + vk14::memory_size_string(usage) + " of " + vk14::memory_size_string(budget) + (ctx.device.memory_budget_enabled() ? "" : " (no budget from the driver)") + "\n" + "images: " + std::to_string(ctx.images.size()) +
", uploaded " + vk14::memory_size_string(ctx.uploads.uploaded()) + ", waiting for the gpu to be deleted: " + std::to_string(ctx.deletions.size()) + "\n" + (ctx.textures ? ctx.textures->info() + "\n" : std::string{}) + ctx.shaders.info() +
"\n" + ctx.pipelines.info() + "\n" + "frame ring: " + vk14::memory_size_string(ctx.ring.used()) + " of " + vk14::memory_size_string(ctx.ring.capacity()) + "\n";
"\n" + ctx.pipelines.info() + "\n" + background_info() + "frame ring: " + vk14::memory_size_string(ctx.ring.used()) + " of " + vk14::memory_size_string(ctx.ring.capacity()) + "\n";
for (auto const &window : ctx.windows)
{
auto const extent{window->extent()};
@@ -1065,6 +1191,49 @@ void vulkan14_renderer::update_info()
m_infostats += "\n" + scene_info() + cull_info();
}
std::string vulkan14_renderer::background_info() const
{
auto const &ctx{*m_context};
std::string info;
if (ctx.background.running())
{
auto const pending{ctx.pending.made_count.load()};
info += "background threads: " + std::to_string(ctx.background.pending()) + " tasks waiting; pipelines made ahead: " + std::to_string(ctx.warm_made.load()) + ", made late: " +
std::to_string(ctx.pipelines_late.load()) + "; textures made: " + std::to_string(pending) + ", draws without theirs: " + std::to_string(ctx.pending.late_count.load()) + "\n";
}
else
{
info += "background threads: off (gfx.vulkan.asyncresources)\n";
}
if (ctx.walks.running())
{
info += "walkers: " + std::to_string(m_walkpasses) + " passes in the last frame, " + std::to_string(vk14::walk_threads()) + (vk14::walk_threads() == 1 ? " thread" : " threads") +
", the main thread waited for them: " + to_string(m_walkwait, 2) + " ms\n";
}
else
{
info += "walkers: off (gfx.vulkan.parallelpasses)\n";
}
if (ctx.models.running())
{
info += "helpers of the walks: " + std::to_string(m_helpedgroups) + " groups of the cullings walked by them in the last frame\n";
}
else
{
info += "helpers of the walks: off (gfx.vulkan.parallelmodels)\n";
}
if (ctx.async_compute)
{
info += std::string("async compute: the sky and the weather on the compute queue (family ") + std::to_string(ctx.device.families().compute) + "), " +
(m_asyncframe ? "in the last frame" : "not in the last frame") + "\n";
}
else
{
info += (Global.gfx_vulkan_asynccompute ? "async compute: off (no compute family of its own)\n" : "async compute: off (gfx.vulkan.asynccompute)\n");
}
return info;
}
bool vulkan14_renderer::Debug_Ui_State(std::optional<bool> State)
{
if (State)

View File

@@ -36,8 +36,10 @@ http://mozilla.org/MPL/2.0/.
#include "gl/ubo.h"
#include <array>
#include <functional>
class world_environment;
struct texture_source;
struct VkRenderingInfo;
namespace vk14
{
@@ -50,12 +52,17 @@ enum class blend_mode;
class instance_cull;
struct image_use;
struct ray_draw;
struct chunk_source;
class command_stream;
} // namespace vk14
class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
{
public:
vulkan14_renderer();
// a walker (the stage M2): walks the passes the main renderer gives it on a thread of its own, drawing with what the main renderer
// draws with (the objects of the api, the geometry, the materials, the textures). it isn't a renderer of the application
explicit vulkan14_renderer(vulkan14_renderer &Main);
~vulkan14_renderer() override;
// lifecycle
@@ -184,6 +191,12 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
void walk_light_query(TSubModel *Submodel, bool const Begin) override;
void walk_editor_cursor() override;
gfx::geometrybank_manager &walk_geometry() override;
bool walk_indirect_source(gfx::geometry_handle const &Geometry, gfx::indirect_source &Source) override;
// the walks of the groups of a culling split among the helpers (vulkan14walkers.cpp, the stage M3, gfx.vulkan.parallelmodels)
bool walk_groups_parallel(std::size_t const Groups) override;
void walk_groups_run(std::size_t const Tasks, std::function<void(gfx::scene_walk &Helper, std::size_t const Task)> const &Work) override;
// the chunk with its data on the gpu (vk14::vulkan14_geometrybank::source()), its bank marked as used
bool chunk_of(gfx::geometry_handle const &Geometry, vk14::chunk_source &Chunk);
bool walk_blending_enabled() const override;
void walk_region_begin() override;
bool walk_instances_begin() override;
@@ -193,6 +206,7 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
// rest): the cullings of the passes and the depth pyramid, made when the renderer starts
bool init_cull();
void destroy_cull();
void ensure_cull();
// the rendering of the pass under way, kept to be begun again where it was: it's ended for the compute passes of the culling
// in the middle of the walk, and begun again with what it drew loaded, with the state of the draws as it was
void remember_rendering(VkRenderingInfo const &Info, std::vector<vk14::image_use> const &Uses);
@@ -246,6 +260,7 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
void render_headlight_shadow(std::size_t const Slot, light_array::light_record const &Light, TDynamicObject *Cab);
vk14::instance_cull *light_shadow_cull(std::vector<std::unique_ptr<vk14::instance_cull>> &Culls, std::size_t const Index, bool const Enabled);
void begin_local_shadow_rendering();
void begin_local_shadow_target();
void end_local_shadow_rendering();
void ready_lamp_maps();
bool m_cubeshadows{false}; // the shadow pass draws to a cube map, with the programs of the cube maps
@@ -267,6 +282,67 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
float m_shadowslicesize{0.f};
float m_shadowdrawnsize{0.f};
// the passes of the sky and the weather of this frame go to the compute queue (the async compute, M6, gfx.vulkan.asynccompute): the
// frame is split before the first pass which reads what they make
bool m_asyncframe{false};
void split_frame();
// the passes walked by the walkers (vulkan14walkers.cpp, the stage M2, gfx.vulkan.parallelpasses): the sun cascades and the shadow
// maps of the lamps and the headlights, walked at the same time as the main thread walks the colour pass, each recorded to a stream
// of its own spliced into the commands of the frame where it would have been recorded (vk14::command_stream::splice()). the main
// thread sets up a pass as it would for itself, hands the state of the frame over to a walker and goes on (gfx::walk_sync); the
// walkers are joined once the colour pass of the main view is recorded
// whether the passes of the main view of this frame go to the walkers
bool walks_wanted() const;
// what a pass of a walker is, for what the main renderer counts of it once it's done
enum class walk_kind
{
other,
lamp,
headlight
};
// a walker given the pass Job, with the state of the frame of this renderer, recorded to a stream spliced here
void fork_walk(std::function<void(vulkan14_renderer &)> Job, walk_kind const Kind = walk_kind::other);
// waits for the walkers, takes what they did over
void join_walks();
// the state of the frame a walker takes from the main renderer, before it walks a pass; and a helper from the walk it works for
void take_state(vulkan14_renderer const &Main);
void take_helper_state(vulkan14_renderer const &Walk);
// the helpers of the walks of this renderer (M3)
std::vector<std::unique_ptr<vulkan14_renderer>> m_helpers;
// for the panel: the groups the helpers walked in the last frame; the groups of the walk under way
std::size_t m_helpedgroups{0};
std::size_t m_helpgroups{0};
// the passes the walkers walk, as the main renderer walked them (render_shadows(), render_lamp_shadow(), render_headlight_shadow())
void walk_cascade(std::size_t const Index);
void walk_lamp(bool const Vehicles, std::vector<TDynamicObject *> const &Dynamics, TDynamicObject *Cab, TAnimModel const *Owner, vk14::instance_cull *Cull);
void walk_headlight(TDynamicObject const *Owner, TDynamicObject *Cab, vk14::instance_cull *Cull);
// the walk lock taken by the main thread for the frame (walk_sync::open()) and the walkers given passes in it
bool m_walkwindow{false};
std::size_t m_walkjobs{0};
std::vector<std::unique_ptr<vulkan14_renderer>> m_walkers;
// the streams of the passes of the walkers, one each, kept by the frames in flight
std::vector<std::vector<std::unique_ptr<vk14::command_stream>>> m_walkstreams;
// for the panel: the passes the walkers walked in the last frame, the time the main thread waited for them, averaged
std::size_t m_walkpasses{0};
double m_walkwait{0.0};
// a pass the walker failed to walk (its stream is dropped, the pass left out of the frame)
bool m_walkfailed{false};
// of a walker: what its pass is, and the state of the thread of the main renderer it takes when it begins (the matrix stack, the
// state of the walk of the submodels, both per thread)
walk_kind m_walkkind{walk_kind::other};
opengl_matrices m_forkmatrices;
struct submodel_state
{
std::size_t instance{0};
std::uint32_t variantseed{0};
material_handle const *skins{nullptr};
int alpha{0};
float squaredistance{0.f};
TModel3d *root{nullptr};
std::string *text{nullptr};
} m_forksubmodels;
// the scene (vulkan14scene.cpp)
bool init_scene(std::string &Reason);
// what the renderer prepares for the frames once it's there: the viewport of the main window, the common textures and geometry
@@ -274,6 +350,8 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
void destroy_scene();
// the scene of the frame: the passes of the viewports
void render_scene();
// a walk of the region (Render() or Render_Alpha()) whose draws may wait for their pipelines and textures (the stage M5)
void walk_region(scene::basic_region *Region, bool const Alpha);
void setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear = 0.f, float const Zfar = 1.f, bool const Ignoredebug = false);
void setup_matrices();
// the colour of a viewport, with the passes it needs first: the sun shadows, the environment map, the light clusters
@@ -415,20 +493,32 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
bool bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology);
void apply_draw_state();
void push_textures();
bool texture_ready(texture_source &Texture);
texture_source &used_texture(texture_handle const Texture);
std::uint32_t push_draw_block();
void open_instance_window();
void update_texture_release();
std::unique_ptr<context> m_context;
// the objects of the vulkan api: made by the main renderer, taken from it by a walker
std::unique_ptr<context> m_contextstore;
context *m_context{nullptr};
// the main renderer, for a walker
vulkan14_renderer *m_main{nullptr};
// what this renderer records to: the commands of the frame, or the pass given to a walker
vk14::command_stream *m_commands{nullptr};
// the light of the sky and the weather of the frame, shared with the opengl renderer
gfx::sky_light m_skylight;
gfx::weather_state m_weatherstate;
// the environment map prefiltered with the GGX lobe is bound for the pass (light_data.env_ggx)
bool m_envmap_bound{false};
// the data of the geometry, which the simulation reads back, and the banks with it on the gpu
gfx::geometrybank_manager m_geometry;
std::vector<gfx::geometry_bank *> m_banks;
material_manager m_materials;
// the data of the geometry, which the simulation reads back, and the banks with it on the gpu; the materials. a walker has those
// of the main renderer
gfx::geometrybank_manager m_geometrystore;
gfx::geometrybank_manager &m_geometry{m_geometrystore};
std::vector<gfx::geometry_bank *> m_bankstore;
std::vector<gfx::geometry_bank *> &m_banks{m_bankstore};
material_manager m_materialstore;
material_manager &m_materials{m_materialstore};
material_handle m_invalid_material{null_handle};
std::vector<viewport_config> m_viewports;
viewport_config *m_current_viewport{nullptr};
@@ -519,6 +609,8 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk
debug_stats m_laststats;
void update_info();
// the lines of the debug panel about the threads of the renderer other than the recording ones (the stages M2, M3, M5, M6)
std::string background_info() const;
// the debug windows (vulkan14debug.cpp, opengl46_renderer::draw_debug_ui()), the lines of the cullings and of the scene for them
// and for the statistics, the counters of the light clusters read back, and the heatmap of the clusters over the output of a
// viewport (opengl46_renderer::Render_cluster_heatmap())

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,7 @@ http://mozilla.org/MPL/2.0/.
#include <filesystem>
#include <fstream>
#include <sstream>
#include <thread>
namespace vk14
{
@@ -103,7 +104,8 @@ void write_file(std::string const &Path, void const *Data, std::size_t const Siz
{
std::error_code error;
std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), error);
auto const temporary{Path + ".tmp"};
// a temporary file of each thread: two of them may write the same file of the cache at once
auto const temporary{Path + ".tmp" + std::to_string(std::hash<std::thread::id>{}(std::this_thread::get_id()))};
{
std::ofstream file(temporary, std::ios::binary | std::ios::trunc);
if (false == file.is_open())
@@ -218,16 +220,29 @@ void shader_library::shutdown()
}
}
std::shared_ptr<shader_code const> shader_library::find(std::string const &Name, std::string const &Defines) const
{
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_shaders.find(Name + "|" + Defines)};
return (lookup != m_shaders.end() ? lookup->second : nullptr);
}
std::shared_ptr<shader_code const> shader_library::get(std::string const &Name, std::string const &Defines)
{
auto const key{Name + "|" + Defines};
auto const lookup{m_shaders.find(key)};
if (lookup != m_shaders.end())
{
return lookup->second;
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_shaders.find(key)};
if (lookup != m_shaders.end())
{
return lookup->second;
}
}
// read or compiled outside of the lock: two threads asking for the same shader at once both make it, the first one kept
auto code{std::make_shared<shader_code>()};
code->name = Name;
code->defines = Defines;
code->source = gfx::preprocess_shader(gfx::shader_api::vulkan14, Name, Defines, m_preamble, true);
code->stage = stage_bit(code->source.stage);
auto const label{Name + (code->source.variant.empty() ? "" : " (" + code->source.variant + ")")};
@@ -243,7 +258,6 @@ std::shared_ptr<shader_code const> shader_library::get(std::string const &Name,
if (false == code->spirv.empty())
{
code->origin = "binary";
++m_frombinary;
}
}
if (code->spirv.empty())
@@ -260,20 +274,24 @@ std::shared_ptr<shader_code const> shader_library::get(std::string const &Name,
if (false == code->spirv.empty())
{
code->origin = "cache";
++m_fromcache;
}
else
{
code->spirv = compile(code->source);
code->origin = "compiled";
++m_compiled;
write_file(cachepath, code->spirv.data(), code->spirv.size() * sizeof(std::uint32_t));
}
}
// the text isn't needed any more
code->source.text = std::string();
m_shaders.emplace(key, code);
return code;
std::lock_guard<std::mutex> lock(m_mutex);
auto const [entry, added]{m_shaders.emplace(key, code)};
if (added)
{
auto const origin{std::string(code->origin)};
++(origin == "binary" ? m_frombinary : origin == "cache" ? m_fromcache : m_compiled);
}
return entry->second;
}
std::vector<std::uint32_t> shader_library::compile(gfx::shader_source const &Source) const
@@ -314,6 +332,7 @@ std::vector<std::uint32_t> shader_library::compile(gfx::shader_source const &Sou
std::string shader_library::info() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return "shaders: " + std::to_string(m_shaders.size()) + " (" + std::to_string(m_frombinary) + " binaries, " + std::to_string(m_fromcache) + " from the cache, " + std::to_string(m_compiled) + " compiled)";
}
@@ -478,13 +497,42 @@ std::vector<VkDynamicState> const dynamic_states{VK_DYNAMIC_STATE_VIEWPORT,
} // namespace
bool pipeline_library::find(pipeline_desc const &Desc, VkPipeline &Pipeline) const
{
auto const key{key_of(Desc)};
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_pipelines.find(key)};
if (lookup == m_pipelines.end())
{
return false;
}
Pipeline = lookup->second;
return true;
}
VkPipeline pipeline_library::keep(std::uint64_t const Key, VkPipeline const Pipeline, double const Milliseconds)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_creationms += Milliseconds;
auto const [entry, added]{m_pipelines.emplace(Key, Pipeline)};
if ((false == added) && (Pipeline != VK_NULL_HANDLE) && (entry->second != Pipeline))
{
// made by another thread meanwhile
vkDestroyPipeline(m_device->handle(), Pipeline, nullptr);
}
return entry->second;
}
VkPipeline pipeline_library::get(pipeline_desc const &Desc)
{
auto const key{key_of(Desc)};
auto const lookup{m_pipelines.find(key)};
if (lookup != m_pipelines.end())
{
return lookup->second;
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_pipelines.find(key)};
if (lookup != m_pipelines.end())
{
return lookup->second;
}
}
auto const start{std::chrono::steady_clock::now()};
@@ -666,9 +714,7 @@ VkPipeline pipeline_library::get(pipeline_desc const &Desc)
{
name_object(m_device->handle(), VK_OBJECT_TYPE_PIPELINE, handle_value(pipeline), Desc.name);
}
m_creationms += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
m_pipelines.emplace(key, pipeline);
return pipeline;
return keep(key, pipeline, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count());
}
VkPipeline pipeline_library::compute(std::shared_ptr<shader_code const> const &Shader, VkPipelineLayout Layout, std::vector<std::uint32_t> const &Specialization, std::string const &Name)
@@ -680,10 +726,13 @@ VkPipeline pipeline_library::compute(std::shared_ptr<shader_code const> const &S
{
hash_value(key, value);
}
auto const lookup{m_pipelines.find(key)};
if (lookup != m_pipelines.end())
{
return lookup->second;
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_pipelines.find(key)};
if (lookup != m_pipelines.end())
{
return lookup->second;
}
}
auto const start{std::chrono::steady_clock::now()};
@@ -722,13 +771,12 @@ VkPipeline pipeline_library::compute(std::shared_ptr<shader_code const> const &S
{
name_object(m_device->handle(), VK_OBJECT_TYPE_PIPELINE, handle_value(pipeline), Name);
}
m_creationms += std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
m_pipelines.emplace(key, pipeline);
return pipeline;
return keep(key, pipeline, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count());
}
std::string pipeline_library::info() const
{
std::lock_guard<std::mutex> lock(m_mutex);
char buffer[32] = {};
std::snprintf(buffer, sizeof(buffer), "%.1f", m_creationms);
return "pipelines: " + std::to_string(m_pipelines.size()) + " (made in " + buffer + " ms)";

View File

@@ -15,7 +15,11 @@ http://mozilla.org/MPL/2.0/.
// cache. the pipelines are made from a description of what isn't dynamic (the shaders, the layout of the vertices, the class
// of the primitives, the blending, the formats of the attachments, the samples); the rest of the state is dynamic. the
// VkPipelineCache is read from and written to the directory of the user, and thrown away when it comes from another device
// or driver. no VkShaderModule: the code goes to the stages of the pipelines (maintenance5)
// or driver. no VkShaderModule: the code goes to the stages of the pipelines (maintenance5).
//
// both libraries may be asked from any thread (the stage M5 of the plan: the pipelines of the scene made by the background
// threads): their maps are guarded, the shaders are compiled and the pipelines made outside of the locks (the VkPipelineCache
// is synchronized by vulkan)
#pragma once
@@ -23,6 +27,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/common/shadersource.h"
#include <memory>
#include <mutex>
#include <unordered_map>
namespace vk14
@@ -38,6 +43,9 @@ struct shader_code
std::vector<std::uint32_t> spirv;
// where it came from: "binary", "cache", "compiled"
char const *origin{""};
// what it was asked for by (shader_library::get()), for the list of the pipelines made in a session
std::string name;
std::string defines;
};
class shader_library
@@ -53,12 +61,15 @@ class shader_library
void shutdown();
// the shader of shaders/vk14 (Defines: the variant). throws gfx::shader_exception
std::shared_ptr<shader_code const> get(std::string const &Name, std::string const &Defines = {});
// the shader, when it was got before; nothing is loaded or compiled
std::shared_ptr<shader_code const> find(std::string const &Name, std::string const &Defines = {}) const;
// what was loaded from where, for the debug panel
std::string info() const;
private:
std::vector<std::uint32_t> compile(gfx::shader_source const &Source) const;
mutable std::mutex m_mutex;
std::string m_preamble;
std::string m_cachedir;
std::string m_compilerversion;
@@ -123,12 +134,17 @@ class pipeline_library
void shutdown();
// the pipeline of the description, made at the first call (from the cache on the disk, when it has it). null when it can't be made
VkPipeline get(pipeline_desc const &Desc);
// the pipeline of the description when it was made already (Pipeline: it, null when it couldn't be); false: not made yet
bool find(pipeline_desc const &Desc, VkPipeline &Pipeline) const;
VkPipeline compute(std::shared_ptr<shader_code const> const &Shader, VkPipelineLayout Layout, std::vector<std::uint32_t> const &Specialization = {}, std::string const &Name = {});
std::string info() const;
private:
std::uint64_t key_of(pipeline_desc const &Desc) const;
// the pipeline made for the key by another thread meanwhile is kept, the one of the caller destroyed
VkPipeline keep(std::uint64_t Key, VkPipeline Pipeline, double Milliseconds);
mutable std::mutex m_mutex;
device const *m_device{nullptr};
VkPipelineCache m_cache{VK_NULL_HANDLE};
std::string m_cachepath;

View File

@@ -85,11 +85,11 @@ bool vulkan14_renderer::init_sky(std::string &Reason)
auto const device{ctx.device.handle()};
auto const frames{static_cast<std::uint32_t>(ctx.frames.size())};
// the buffers the shading reads
if ((false == sky.sky.create(ctx.memory, sizeof(gl::sky_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "sky ambient")) ||
(false == sky.atmosphere.create(ctx.memory, sizeof(gl::atmosphere_ubs), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "atmosphere")) ||
(false == sky.result.create(ctx.memory, sizeof(gl::atmosphere_result), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "atmosphere result")) ||
(false == sky.result_readback.create(ctx.memory, sizeof(gl::atmosphere_result), frames, "atmosphere result readback")) ||
// the buffers the shading reads; what the passes of the sky make is shared with the compute queue (the async compute of M6)
if ((false == sky.sky.create(ctx.memory, sizeof(gl::sky_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "sky ambient", true)) ||
(false == sky.atmosphere.create(ctx.memory, sizeof(gl::atmosphere_ubs), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "atmosphere", true)) ||
(false == sky.result.create(ctx.memory, sizeof(gl::atmosphere_result), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT, vk14::memory_kind::device, "atmosphere result", true)) ||
(false == sky.result_readback.create(ctx.memory, sizeof(gl::atmosphere_result), frames, "atmosphere result readback", true)) ||
(false == exposure.data.create(ctx.memory, sizeof(gl::exposure_data), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, vk14::memory_kind::device, "exposure")) ||
(false == exposure.readback.create(ctx.memory, 4 * sizeof(float), frames, "exposure readback")))
{
@@ -111,6 +111,7 @@ bool vulkan14_renderer::init_sky(std::string &Reason)
vk14::image_desc desc;
desc.format = VK_FORMAT_R16G16B16A16_SFLOAT;
desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
desc.shared = true;
desc.extent = {gl::ATMOSPHERE_TRANSMITTANCE_WIDTH, gl::ATMOSPHERE_TRANSMITTANCE_HEIGHT, 1};
if (false == make_image(sky.transmittance, desc, "atmosphere transmittance"))
{
@@ -245,7 +246,7 @@ void vulkan14_renderer::clear_sky_images()
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &sky{scene.sky};
auto const commands{ctx.commands};
auto const commands{m_commands};
std::vector<vk14::image_use> uses;
for (auto *image : {sky.transmittance.get(), sky.multiscattering.get(), sky.skyview.get(), sky.ggx.get()})
{
@@ -294,7 +295,7 @@ void vulkan14_renderer::update_sky()
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &sky{scene.sky};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const &sun{simulation::Environment.sun()};
auto const sundirection{sun.getDirection()};
@@ -353,7 +354,7 @@ void vulkan14_renderer::prefilter_environment()
{
return;
}
auto const commands{ctx.commands};
auto const commands{m_commands};
ctx.passes.begin_pass("envmap ggx", {vk14::use_of(*scene.env_map, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, compute_stage, sampled_access),
vk14::use_of(*sky.ggx, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)});
for (std::size_t level = 0; level < gl::ENVMAP_GGX_LEVELS; ++level)
@@ -430,7 +431,7 @@ void vulkan14_renderer::measure_exposure(vk14::image &Image)
{
auto &ctx{*m_context};
auto &exposure{ctx.scene.exposure};
auto const commands{ctx.commands};
auto const commands{m_commands};
float data[4];
if (exposure.readback.pop(ctx.completed(), data, sizeof(data)))
{

View File

@@ -0,0 +1,114 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "rendering/vulkan14/vulkan14tasks.h"
#include "utilities/Logs.h"
#include <exception>
namespace vk14
{
background_tasks::~background_tasks()
{
stop();
}
void background_tasks::start(std::size_t const Threads)
{
if (running())
{
return;
}
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stop = false;
}
for (std::size_t i = 0; i < Threads; ++i)
{
m_threads.emplace_back([this]() { work(); });
}
}
void background_tasks::stop()
{
{
std::lock_guard<std::mutex> lock(m_mutex);
m_stop = true;
m_queue.clear();
}
m_wake.notify_all();
for (auto &thread : m_threads)
{
thread.join();
}
m_threads.clear();
}
void background_tasks::add(std::function<void()> Task, bool const Urgent)
{
{
std::lock_guard<std::mutex> lock(m_mutex);
if (Urgent)
{
m_queue.emplace_front(std::move(Task));
}
else
{
m_queue.emplace_back(std::move(Task));
}
}
m_wake.notify_one();
}
void background_tasks::wait_idle()
{
std::unique_lock<std::mutex> lock(m_mutex);
m_idle.wait(lock, [this]() { return m_queue.empty() && (m_busy == 0); });
}
std::size_t background_tasks::pending() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_queue.size() + m_busy;
}
void background_tasks::work()
{
std::unique_lock<std::mutex> lock(m_mutex);
while (true)
{
m_wake.wait(lock, [this]() { return m_stop || (false == m_queue.empty()); });
if (m_stop)
{
return;
}
auto task{std::move(m_queue.front())};
m_queue.pop_front();
++m_busy;
lock.unlock();
try
{
task();
}
catch (std::exception const &Exception)
{
ErrorLog(std::string("vulkan: a background task failed: ") + Exception.what());
}
lock.lock();
--m_busy;
if (m_queue.empty() && (m_busy == 0))
{
m_idle.notify_all();
}
}
}
} // namespace vk14

View File

@@ -0,0 +1,62 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the background threads of the vulkan renderer (the stage M5 of the plan): the work which would hold up a frame, done while the
// frames go on. the pipelines of the scene and the textures made when a draw first needs them, the pipelines of the last session
// made ahead. what a task makes is taken over by the thread of the renderer when it's done (the frame after, as a rule); a task
// itself touches only what it was given and what is guarded for it
#pragma once
#include <condition_variable>
#include <cstddef>
#include <deque>
#include <functional>
#include <mutex>
#include <thread>
#include <vector>
namespace vk14
{
class background_tasks
{
public:
background_tasks() = default;
background_tasks(background_tasks const &) = delete;
background_tasks &operator=(background_tasks const &) = delete;
~background_tasks();
void start(std::size_t Threads);
// the tasks under way are finished, the ones waiting dropped
void stop();
bool running() const
{
return false == m_threads.empty();
}
// Urgent: before the tasks waiting (what a draw waits for), otherwise after them (what's made ahead)
void add(std::function<void()> Task, bool Urgent = true);
// waits until there's nothing waiting and nothing under way
void wait_idle();
// the tasks waiting and under way
std::size_t pending() const;
private:
void work();
mutable std::mutex m_mutex;
std::condition_variable m_wake;
std::condition_variable m_idle;
std::deque<std::function<void()>> m_queue;
std::vector<std::thread> m_threads;
std::size_t m_busy{0};
bool m_stop{false};
};
} // namespace vk14

View File

@@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/vulkan14/vulkan14texture.h"
#include "rendering/vulkan14/vulkan14commands.h"
#include "rendering/vulkan14/vulkan14device.h"
#include "rendering/vulkan14/vulkan14tasks.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
@@ -28,8 +29,26 @@ void uploader::init(allocator const &Allocator, deletion_queue &Deletions, VkPhy
m_physical = Physical;
}
void uploader::reset()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_allocator = nullptr;
m_deletions = nullptr;
m_physical = VK_NULL_HANDLE;
m_commands = nullptr;
m_value = 0;
m_recorded = false;
m_buffercopies = false;
m_pending.clear();
m_uploaded = 0;
m_blittable.clear();
m_hostcopy = false;
m_hostcopyable.clear();
}
bool uploader::can_blit(VkFormat Format) const
{
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_blittable.find(Format)};
if (lookup != m_blittable.end())
{
@@ -43,8 +62,48 @@ bool uploader::can_blit(VkFormat Format) const
return result;
}
bool uploader::can_host_copy(VkFormat Format) const
{
if (false == m_hostcopy)
{
return false;
}
std::lock_guard<std::mutex> lock(m_mutex);
auto const lookup{m_hostcopyable.find(Format)};
if (lookup != m_hostcopyable.end())
{
return lookup->second;
}
// the format filled by the host, and an image which can be is as fast for the gpu as one which can't
VkFormatProperties3 properties3{};
properties3.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_3;
VkFormatProperties2 properties{};
properties.sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2;
properties.pNext = &properties3;
vkGetPhysicalDeviceFormatProperties2(m_physical, Format, &properties);
auto result{(properties3.optimalTilingFeatures & VK_FORMAT_FEATURE_2_HOST_IMAGE_TRANSFER_BIT) != 0};
if (result)
{
VkHostImageCopyDevicePerformanceQuery performance{};
performance.sType = VK_STRUCTURE_TYPE_HOST_IMAGE_COPY_DEVICE_PERFORMANCE_QUERY;
VkImageFormatProperties2 imageproperties{};
imageproperties.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2;
imageproperties.pNext = &performance;
VkPhysicalDeviceImageFormatInfo2 info{};
info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2;
info.format = Format;
info.type = VK_IMAGE_TYPE_2D;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_HOST_TRANSFER_BIT;
result = (vkGetPhysicalDeviceImageFormatProperties2(m_physical, &info, &imageproperties) == VK_SUCCESS) && (performance.optimalDeviceAccess == VK_TRUE);
}
m_hostcopyable.emplace(Format, result);
return result;
}
void uploader::begin_frame(command_stream *Commands, std::uint64_t Value)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_commands = Commands;
m_value = Value;
m_recorded = false;
@@ -58,6 +117,7 @@ void uploader::begin_frame(command_stream *Commands, std::uint64_t Value)
bool uploader::end_frame()
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_buffercopies && (m_commands != nullptr))
{
// the draws of the frame read what was copied
@@ -100,6 +160,7 @@ bool uploader::upload_buffer(std::shared_ptr<buffer> const &Target, void const *
std::memcpy(upload.staging->mapped(), Data, static_cast<std::size_t>(Size));
upload.staging->flush(0, Size);
m_uploaded += Size;
std::lock_guard<std::mutex> lock(m_mutex);
if (m_commands != nullptr)
{
record(upload);
@@ -113,19 +174,45 @@ bool uploader::upload_buffer(std::shared_ptr<buffer> const &Target, void const *
bool uploader::upload(std::shared_ptr<image> const &Image, void const *Data, VkDeviceSize Size, std::vector<level> const &Levels, bool const Mipmaps)
{
job upload;
upload.target = Image;
upload.levels = Levels;
upload.mipmaps = Mipmaps;
upload.staging = std::make_shared<buffer>();
if ((false == upload.staging->create(*m_allocator, Size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload)) || (upload.staging->mapped() == nullptr))
prepared upload;
if (false == prepare(Image, Data, Size, Levels, Mipmaps, upload))
{
log_error("no staging buffer of " + memory_size_string(Size) + " for an upload");
return false;
}
std::memcpy(upload.staging->mapped(), Data, static_cast<std::size_t>(Size));
upload.staging->flush(0, Size);
submit(std::move(upload));
return true;
}
bool uploader::prepare(std::shared_ptr<image> const &Image, void const *Data, VkDeviceSize Size, std::vector<level> const &Levels, bool const Mipmaps, prepared &Upload)
{
Upload.target = Image;
Upload.levels = Levels;
Upload.mipmaps = Mipmaps;
Upload.staging = std::make_shared<buffer>();
if ((false == Upload.staging->create(*m_allocator, Size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, memory_kind::upload)) || (Upload.staging->mapped() == nullptr))
{
log_error("no staging buffer of " + memory_size_string(Size) + " for an upload");
Upload = prepared{};
return false;
}
std::memcpy(Upload.staging->mapped(), Data, static_cast<std::size_t>(Size));
Upload.staging->flush(0, Size);
m_uploaded += Size;
return true;
}
void uploader::submit(prepared &&Upload)
{
if ((Upload.target == nullptr) || (Upload.staging == nullptr))
{
return;
}
job upload;
upload.target = std::move(Upload.target);
upload.staging = std::move(Upload.staging);
upload.levels = std::move(Upload.levels);
upload.mipmaps = Upload.mipmaps;
std::lock_guard<std::mutex> lock(m_mutex);
if (m_commands != nullptr)
{
record(upload);
@@ -134,7 +221,6 @@ bool uploader::upload(std::shared_ptr<image> const &Image, void const *Data, VkD
{
m_pending.emplace_back(std::move(upload));
}
return true;
}
void uploader::record(job &Job)
@@ -270,6 +356,7 @@ void uploader::record(job &Job)
std::uint64_t image_table::add(std::shared_ptr<image> const &Image, VkSampler Sampler)
{
std::lock_guard<std::mutex> lock(m_mutex);
std::uint64_t id;
if (false == m_free.empty())
{
@@ -287,6 +374,7 @@ std::uint64_t image_table::add(std::shared_ptr<image> const &Image, VkSampler Sa
void image_table::replace(std::uint64_t Id, std::shared_ptr<image> const &Image, VkSampler Sampler)
{
std::lock_guard<std::mutex> lock(m_mutex);
if ((Id > 0) && (Id < m_entries.size()))
{
m_entries[Id] = entry{Image, Sampler};
@@ -295,6 +383,7 @@ void image_table::replace(std::uint64_t Id, std::shared_ptr<image> const &Image,
void image_table::remove(std::uint64_t Id, deletion_queue &Deletions, std::uint64_t Value)
{
std::lock_guard<std::mutex> lock(m_mutex);
if ((Id == 0) || (Id >= m_entries.size()) || (m_entries[Id].target == nullptr))
{
return;
@@ -306,6 +395,7 @@ void image_table::remove(std::uint64_t Id, deletion_queue &Deletions, std::uint6
image_table::entry const *image_table::find(std::uint64_t Id) const
{
std::lock_guard<std::mutex> lock(m_mutex);
if ((Id == 0) || (Id >= m_entries.size()) || (m_entries[Id].target == nullptr))
{
return nullptr;
@@ -315,6 +405,7 @@ image_table::entry const *image_table::find(std::uint64_t Id) const
void image_table::clear()
{
std::lock_guard<std::mutex> lock(m_mutex);
m_entries.assign(1, entry{});
m_free.clear();
}
@@ -386,15 +477,184 @@ texture_format texture_format_of(GLint Format, GLint Hint)
vulkan14_texture::~vulkan14_texture()
{
// a background thread making the texture is done with it first
wait_job();
if (m_resources.pending != nullptr)
{
std::lock_guard<std::mutex> lock(m_resources.pending->mutex);
auto &list{m_resources.pending->textures};
list.erase(std::remove(list.begin(), list.end(), this), list.end());
for (auto *list : {&m_resources.pending->textures, &m_resources.pending->made})
{
list->erase(std::remove(list->begin(), list->end(), this), list->end());
}
}
}
void vulkan14_texture::wait_job() const
{
while ((m_job == job_running) || (m_job == job_making))
{
std::this_thread::yield();
}
}
bool vulkan14_texture::claim()
{
auto const renderer{std::this_thread::get_id() == m_resources.thread};
while (true)
{
auto state{m_job.load()};
if (state == job_done)
{
if (false == renderer)
{
// what a background thread made is taken over by the thread of the renderer
return false;
}
publish();
continue;
}
if (state != job_none)
{
// made by another thread at the moment (a background thread, or the walk of a pass on another thread)
std::this_thread::yield();
continue;
}
if (m_job.compare_exchange_weak(state, job_making))
{
return true;
}
}
}
bool vulkan14_texture::create(bool const Static)
{
if (m_made)
{
return true;
}
if ((type == "make:") && (std::this_thread::get_id() != m_resources.thread))
{
// the request of the generated content goes from the thread of the renderer
return false;
}
if (false == claim())
{
return false;
}
auto const result{make(false)};
auto ready{true};
switch (result)
{
case make_result::none:
ready = false;
break;
case make_result::done:
break;
case make_result::failed:
is_ready = true;
break;
case make_result::made:
if (type == "make:")
{
// for generated textures send a request to have the actual content of the texture generated
make_request();
}
is_static = Static;
is_ready = true;
break;
}
m_job = job_none;
return ready;
}
bool vulkan14_texture::request_background()
{
if (m_made)
{
return true;
}
auto state{m_job.load()};
if ((state == job_done) && (std::this_thread::get_id() == m_resources.thread))
{
// made meanwhile by a background thread
publish();
if (m_made)
{
return true;
}
state = m_job.load();
}
if (state != job_none)
{
// being made, or made and waiting for the thread of the renderer
if (m_resources.pending != nullptr)
{
++m_resources.pending->late_count;
}
return false;
}
if ((m_resources.background == nullptr) || (false == m_resources.background->running()) || (m_resources.pending == nullptr))
{
return create();
}
// asked for by one of the threads walking the passes at a time
if (false == m_job.compare_exchange_strong(state, job_running))
{
++m_resources.pending->late_count;
return false;
}
++m_resources.pending->late_count;
// the texture outlives the job: its destruction waits for it (the background threads are stopped before the bank goes, too)
m_resources.background->add([this]() {
auto const result{make(true)};
m_madefailed = (result == make_result::failed);
m_madeempty = (result == make_result::none);
std::lock_guard<std::mutex> lock(m_resources.pending->mutex);
m_job = job_done;
m_resources.pending->made.push_back(this);
});
return false;
}
void vulkan14_texture::publish()
{
int expected{job_done};
if (false == m_job.compare_exchange_strong(expected, job_making))
{
return;
}
if (m_madeimage != nullptr)
{
if (m_madestaged)
{
m_resources.uploads->submit(std::move(m_madeupload));
}
m_image = std::move(m_madeimage);
m_id = m_resources.images->add(m_image, m_madesampler);
m_made = true;
if (type == "make:")
{
make_request();
}
is_static = false;
is_ready = true;
++m_resources.pending->made_count;
}
else if (m_madefailed)
{
// counted as made, without an image (on_failed())
is_ready = true;
}
// otherwise there was no data yet: asked for again by the next draw
m_madeimage.reset();
m_madeupload = uploader::prepared{};
m_madestaged = false;
m_madefailed = false;
m_madeempty = false;
m_job = job_none;
}
vulkan14_texture::make_result vulkan14_texture::make(bool const Background)
{
if ((data_state == resource_state::none) && (true == reload_on_use))
{
@@ -408,12 +668,13 @@ bool vulkan14_texture::create(bool const Static)
}
if (m_made)
{
return true;
// made before, or failed while the data was read (on_failed() of a texture without an id leaves the table alone)
return make_result::done;
}
if (data_state != resource_state::good)
{
// don't bother until we have useful texture data
return false;
return make_result::none;
}
auto const format{texture_format_of(data_format, components_hint != 0 ? components_hint : GL_SRGB_ALPHA)};
@@ -422,12 +683,14 @@ bool vulkan14_texture::create(bool const Static)
{
vkGetPhysicalDeviceFormatProperties(m_resources.gpu->physical(), format.format, &properties);
}
auto const failed = [this]() {
on_failed();
return make_result::failed;
};
if ((format.format == VK_FORMAT_UNDEFINED) || ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) == 0))
{
ErrorLog("Bad texture: the format of \"" + name + "\" can't be sampled by the gpu", logtype::texture);
on_failed();
is_ready = true;
return true;
return failed();
}
std::vector<uploader::level> levels;
@@ -478,41 +741,92 @@ bool vulkan14_texture::create(bool const Static)
if (levels.empty() || (size > available) || (data_width == 0) || (data_height == 0))
{
ErrorLog("Bad texture: \"" + name + "\" holds less data than its size needs", logtype::texture);
on_failed();
is_ready = true;
return true;
return failed();
}
// a single level is made into the whole chain on the gpu, as glGenerateMipmap() does for the opengl renderer
auto const generate{(levels.size() == 1) && (false == format.compressed) && m_resources.uploads->can_blit(format.format)};
// the levels the file gives are filled by the host where it can, by a background thread (what the gpu makes goes through the
// commands of a frame)
auto const hostcopy{Background && (false == generate) && m_resources.uploads->can_host_copy(format.format)};
image_desc desc;
desc.format = format.format;
desc.extent = {data_width, data_height, 1};
desc.mips = (generate ? static_cast<std::uint32_t>(std::floor(std::log2(std::max(data_width, data_height)))) + 1 : static_cast<std::uint32_t>(levels.size()));
desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (generate ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0);
desc.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (generate ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0) | (hostcopy ? VK_IMAGE_USAGE_HOST_TRANSFER_BIT : 0);
desc.components = format.components;
auto image{std::make_shared<vk14::image>()};
if (false == image->create(*m_resources.memory, desc, name))
{
ErrorLog("Bad texture: no gpu image for \"" + name + "\"", logtype::texture);
on_failed();
is_ready = true;
return true;
return failed();
}
if (false == m_resources.uploads->upload(image, source, size, levels, generate))
auto staged{false};
if (hostcopy)
{
// straight to the layout the image is sampled in: no command buffer, nothing for the frames to wait for
auto const device{m_resources.gpu->handle()};
VkHostImageLayoutTransitionInfo transition{};
transition.sType = VK_STRUCTURE_TYPE_HOST_IMAGE_LAYOUT_TRANSITION_INFO;
transition.image = image->handle();
transition.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
transition.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
transition.subresourceRange = {image->aspect(), 0, desc.mips, 0, desc.layers};
std::vector<VkMemoryToImageCopy> regions(levels.size());
for (std::size_t i = 0; i < levels.size(); ++i)
{
auto &region{regions[i]};
region.sType = VK_STRUCTURE_TYPE_MEMORY_TO_IMAGE_COPY;
region.pHostPointer = source + levels[i].offset;
region.imageSubresource = {image->aspect(), static_cast<std::uint32_t>(i), 0, desc.layers};
region.imageExtent = levels[i].extent;
}
VkCopyMemoryToImageInfo copy{};
copy.sType = VK_STRUCTURE_TYPE_COPY_MEMORY_TO_IMAGE_INFO;
copy.dstImage = image->handle();
copy.dstImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
copy.regionCount = static_cast<std::uint32_t>(regions.size());
copy.pRegions = regions.data();
if ((false == VK14_VERIFY(vkTransitionImageLayout(device, 1, &transition))) || (false == VK14_VERIFY(vkCopyMemoryToImage(device, &copy))))
{
ErrorLog("Bad texture: \"" + name + "\" couldn't be copied to its gpu image", logtype::texture);
return failed();
}
image->state = image_state{VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT};
}
else if (Background)
{
// the staging buffer filled here, the copies recorded by the thread of the renderer
if (false == m_resources.uploads->prepare(image, source, size, levels, generate, m_madeupload))
{
ErrorLog("Bad texture: \"" + name + "\" couldn't be uploaded", logtype::texture);
return failed();
}
staged = true;
}
else if (false == m_resources.uploads->upload(image, source, size, levels, generate))
{
// never filled, so never sampled
ErrorLog("Bad texture: \"" + name + "\" couldn't be uploaded", logtype::texture);
on_failed();
is_ready = true;
return true;
return failed();
}
// the traits of the file: clamped along s or t
auto const clamped{(traits.find('s') != std::string::npos) || (traits.find('t') != std::string::npos)};
m_image = image;
m_id = m_resources.images->add(image, clamped ? m_resources.sampling->clamp_aniso : m_resources.sampling->repeat_aniso);
m_made = true;
auto const sampler{clamped ? m_resources.sampling->clamp_aniso : m_resources.sampling->repeat_aniso};
if (Background)
{
// the thread of the renderer puts it in the table (publish())
m_madeimage = image;
m_madesampler = sampler;
m_madestaged = staged;
}
else
{
m_image = image;
m_id = m_resources.images->add(image, sampler);
m_made = true;
}
if ((true == Global.ResourceMove) || (false == Global.ResourceSweep) || (true == streamed()))
{
@@ -520,14 +834,7 @@ bool vulkan14_texture::create(bool const Static)
data = std::vector<unsigned char>();
data_state = resource_state::none;
}
if (type == "make:")
{
// for generated textures send a request to have the actual content of the texture generated
make_request();
}
is_static = Static;
is_ready = true;
return true;
return make_result::made;
}
void vulkan14_texture::release()
@@ -569,6 +876,20 @@ void vulkan14_texture::release()
void vulkan14_texture::destroy()
{
wait_job();
if (m_job == job_done)
{
// made and not taken over: what was made goes now
if (m_resources.pending != nullptr)
{
std::lock_guard<std::mutex> lock(m_resources.pending->mutex);
auto &list{m_resources.pending->made};
list.erase(std::remove(list.begin(), list.end(), this), list.end());
}
m_madeimage.reset();
m_madeupload = uploader::prepared{};
m_job = job_none;
}
if (m_id != 0)
{
m_resources.images->remove(m_id, *m_resources.deletions, m_resources.uploads->current_value());

View File

@@ -13,7 +13,12 @@ http://mozilla.org/MPL/2.0/.
// the uploads are copies through staging buffers recorded into the command buffer of uploads of the frame being made, which goes
// to the queue before the commands of the frame in the same submit; an upload asked for between the frames waits for the next
// one. every image has an id in one table (decision K10): ITexture::get_id() of the textures, the ids of the images of the user
// interface and the font of imgui are all ImTextureID, so the user interface draws any of them
// interface and the font of imgui are all ImTextureID, so the user interface draws any of them.
//
// the textures the draws of the walks need first are made by the background threads (gfx.vulkan.asyncresources, the stage M5):
// the data read, the image made and filled by the host (hostImageCopy, where the device and the format allow it) or its staging
// buffer filled, then the thread of the renderer puts the image in the table, with the copies of the staging buffer recorded, before
// the next frame is walked. the draw which asked for it goes without it (the white texture) till then
#pragma once
@@ -21,6 +26,8 @@ http://mozilla.org/MPL/2.0/.
#include "rendering/vulkan14/vulkan14descriptors.h"
#include "model/Texture.h"
#include <atomic>
#include <deque>
#include <memory>
#include <mutex>
#include <thread>
@@ -29,8 +36,10 @@ namespace vk14
{
class device;
class background_tasks;
// the uploads of data to the images
// the uploads of data to the images. asked for by any thread (the walks of the passes make the buffers of the geometry when they're
// first drawn); the staging buffers are made outside of the lock
class uploader
{
public:
@@ -39,8 +48,18 @@ class uploader
VkDeviceSize offset{0}; // in the data
VkExtent3D extent{1, 1, 1};
};
// an upload to an image with its data in a staging buffer, not recorded yet
struct prepared
{
std::shared_ptr<image> target;
std::shared_ptr<buffer> staging;
std::vector<level> levels;
bool mipmaps{false};
};
void init(allocator const &Allocator, deletion_queue &Deletions, VkPhysicalDevice Physical);
// back to the state before init(), the uploads waiting dropped
void reset();
// the commands of the uploads go to Commands until end_frame(); Value: the value of the timeline the gpu reaches when it's done
// with the frame. the uploads waiting from between the frames are recorded first
void begin_frame(command_stream *Commands, std::uint64_t Value);
@@ -54,6 +73,9 @@ class uploader
// copies Data into Image (the levels given, the others made by blits of the first one when Mipmaps), the image left ready to be
// sampled. Data is copied away. false: no staging memory, nothing will be copied
bool upload(std::shared_ptr<image> const &Image, void const *Data, VkDeviceSize Size, std::vector<level> const &Levels, bool const Mipmaps);
// the same in two steps: the staging buffer made and filled (Upload), and the copies recorded. false: no staging memory
bool prepare(std::shared_ptr<image> const &Image, void const *Data, VkDeviceSize Size, std::vector<level> const &Levels, bool const Mipmaps, prepared &Upload);
void submit(prepared &&Upload);
// copies Data into Target, done before the draws of the frame read it (and after the frames sent before did). Data holds the
// pieces of Parts (offset in Target, size) one after another; all of Data goes to the start of Target when there are none.
// false: no staging memory
@@ -65,6 +87,13 @@ class uploader
}
// whether the format can be filtered by blits (the mipmaps made on the gpu)
bool can_blit(VkFormat Format) const;
// whether images of the format are filled by the host (hostImageCopy) straight to the layout they're sampled in, without a loss of
// speed for the gpu. Copyable: the device does that for the layout (decided when the device is made)
void enable_host_copy(bool Copyable)
{
m_hostcopy = Copyable;
}
bool can_host_copy(VkFormat Format) const;
private:
struct job
@@ -79,20 +108,24 @@ class uploader
};
void record(job &Job);
mutable std::mutex m_mutex;
allocator const *m_allocator{nullptr};
deletion_queue *m_deletions{nullptr};
VkPhysicalDevice m_physical{VK_NULL_HANDLE};
command_stream *m_commands{nullptr};
std::uint64_t m_value{0};
std::atomic<std::uint64_t> m_value{0};
bool m_recorded{false};
// buffers were copied to in the frame: the barriers before the first copy and after the last one
bool m_buffercopies{false};
std::vector<job> m_pending;
std::uint64_t m_uploaded{0};
std::atomic<std::uint64_t> m_uploaded{0};
mutable std::unordered_map<VkFormat, bool> m_blittable;
bool m_hostcopy{false};
mutable std::unordered_map<VkFormat, bool> m_hostcopyable;
};
// the images the shaders can be given by an id
// the images the shaders can be given by an id. added to by any thread drawing (a texture made when a draw needs it), removed from
// and replaced in by the thread of the renderer, found by any thread; an entry found stays where it is while the frame is made
class image_table
{
public:
@@ -110,21 +143,27 @@ class image_table
void clear();
std::size_t size() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_entries.size() - m_free.size();
}
private:
std::vector<entry> m_entries{entry{}};
mutable std::mutex m_mutex;
std::deque<entry> m_entries{entry{}};
std::vector<std::uint64_t> m_free;
};
struct vulkan14_texture;
// the textures given content by other threads, waiting for the thread of the renderer
// the textures given content by other threads, waiting for the thread of the renderer; and the ones made by the background threads
struct pending_textures
{
std::mutex mutex;
std::vector<vulkan14_texture *> textures;
std::vector<vulkan14_texture *> made;
// the textures made by the background threads since the start, and how many draws went without one
std::atomic<std::size_t> made_count{0};
std::atomic<std::size_t> late_count{0};
};
// what the images of the renderer need from it
@@ -139,6 +178,8 @@ struct resources
// the thread of the renderer: the content given by other threads waits for it (update_from_memory())
std::thread::id thread;
pending_textures *pending{nullptr};
// the threads making the textures the draws need first (gfx.vulkan.asyncresources), null: made by the draw
background_tasks *background{nullptr};
// the ray tracing is on (gfx.raytracing): the geometry is built into acceleration structures and read by its device address
bool raytracing{false};
};
@@ -163,6 +204,11 @@ struct vulkan14_texture : public texture_source
~vulkan14_texture() override;
bool create(bool const Static = false) override;
// the texture for a draw of the walks: true when it's made, otherwise it's made by the background threads (or made now, without
// them) and the draw goes without it
bool request_background();
// the thread of the renderer takes over what a background thread made of the texture
void publish();
void release() override;
// as the opengl textures give it: one not made yet has no id of 0, which the materials take for a texture which is missing
size_t get_id() const override
@@ -174,6 +220,11 @@ struct vulkan14_texture : public texture_source
{
return m_made;
}
// a background thread makes it: its data isn't for the other threads to read
bool busy() const override
{
return m_job != job_none;
}
void destroy() override;
// the content given by update_from_memory() from another thread, uploaded by the thread of the renderer
@@ -191,11 +242,43 @@ struct vulkan14_texture : public texture_source
private:
void upload_rgba(std::uint8_t const *Data, std::size_t Width, std::size_t Height);
// the data of the texture made into an image (Background: by a background thread, which leaves the images table and the uploads
// of the frames to the thread of the renderer): no data yet, made now, failed now, made (or failed) before
enum class make_result
{
none,
made,
failed,
done
};
make_result make(bool const Background);
// waits for the thread making the texture, if there's one
void wait_job() const;
// the making of the texture taken by the calling thread (create()), after another thread making it is done; false when it was made by
// a background thread and waits for the thread of the renderer
bool claim();
resources const &m_resources;
std::shared_ptr<image> m_image;
std::uint64_t m_id{0};
bool m_made{false};
std::atomic<bool> m_made{false};
// the work on the texture: none, a background thread making it, made by it and waiting for publish(), a thread making it now (create(),
// publish(): the walks of the passes on several threads may need it at once, M2)
enum job_state : int
{
job_none,
job_running,
job_done,
job_making
};
std::atomic<int> m_job{job_none};
// what the background thread made: the image and its sampler, its upload when the host didn't fill it, or that the texture failed
std::shared_ptr<image> m_madeimage;
VkSampler m_madesampler{VK_NULL_HANDLE};
uploader::prepared m_madeupload;
bool m_madestaged{false};
bool m_madefailed{false};
bool m_madeempty{false};
// content from another thread, waiting for the thread of the renderer
std::mutex m_pendingmutex;
std::vector<std::uint8_t> m_pending;

View File

@@ -0,0 +1,402 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
// the passes walked by the walkers (the stage M2 of claude/vulkan14-renderer-plan.md, gfx.vulkan.parallelpasses). the main thread sets
// a pass up as it would walk it itself (the camera, the culling, the map it draws to with its barrier, the timestamps around it), hands
// the state of the frame over to a walker, splices the stream the walker records into the commands of the frame there and goes on with
// the frame; the walkers walk the passes at the same time as the main thread walks the colour pass of the main view, and are joined once
// it's recorded. what the walks share is guarded by gfx::walk_sync; what a walker's draws used is marked by the main renderer after the
// join. with the switch off, or for the other viewports, the passes are walked by the main thread as before
#include "stdafx.h"
#include "rendering/vulkan14/vulkan14context.h"
#include "rendering/vulkan14/vulkan14commands.h"
#include "rendering/common/walksync.h"
#include "model/Model3d.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <exception>
#include <mutex>
#include <thread>
std::size_t vk14::walk_threads()
{
// a pass for each of them: the three cascades, a lamp or two
auto threads{static_cast<std::size_t>(Global.gfx_threads)};
if (threads == 0)
{
auto const cores{static_cast<std::size_t>(std::thread::hardware_concurrency())};
threads = (cores > 3 ? cores - 2 : 1);
}
return std::clamp<std::size_t>(threads, 1, 4);
}
vulkan14_renderer::vulkan14_renderer(vulkan14_renderer &Main) : m_context{Main.m_context}, m_main{&Main}, m_geometry{Main.m_geometry}, m_banks{Main.m_banks}, m_materials{Main.m_materials}
{
m_draw = std::make_unique<draw_state>();
m_draw->textures.fill(null_handle);
}
bool vulkan14_renderer::walks_wanted() const
{
return (m_main == nullptr) && Global.gfx_vulkan_parallelpasses && m_context && m_context->walks.running() && (m_current_viewport != nullptr) && m_current_viewport->main && (m_commands != nullptr);
}
void vulkan14_renderer::take_state(vulkan14_renderer const &Main)
{
// the frame: its viewports, the passes set up for it, the light, the blocks of the scene and of the lights uploaded so far
m_viewports = Main.m_viewports;
m_current_viewport = (Main.m_current_viewport != nullptr) ? &m_viewports[static_cast<std::size_t>(Main.m_current_viewport - Main.m_viewports.data())] : nullptr;
m_renderpass = Main.m_renderpass;
m_colorpass = Main.m_colorpass;
m_shadowpass = Main.m_shadowpass;
m_sunlight = Main.m_sunlight;
m_fogcolor = Main.m_fogcolor;
m_shadowcolor = Main.m_shadowcolor;
scene_ubs = Main.scene_ubs;
light_data = Main.light_data;
m_lightcount = Main.m_lightcount;
m_lightheaders = Main.m_lightheaders;
m_cabambient = Main.m_cabambient;
m_envmap_bound = Main.m_envmap_bound;
m_headlightconfig = Main.m_headlightconfig;
m_raytracingon = Main.m_raytracingon;
m_rtfoliage = Main.m_rtfoliage;
m_rtgather = false;
m_instancecullframe = Main.m_instancecullframe;
m_indirectcolourpending = false;
m_cubeshadows = false;
m_shadowcascade = -1;
m_lightshadowcull = nullptr;
// the textures and the materials the renderer draws with
m_invalid_material = Main.m_invalid_material;
m_suntexture = Main.m_suntexture;
m_moontexture = Main.m_moontexture;
m_smoketexture = Main.m_smoketexture;
m_headlightstexture = Main.m_headlightstexture;
m_precipitationtexture = Main.m_precipitationtexture;
// the walk
model_ubs = Main.model_ubs;
m_framestamp = Main.m_framestamp;
m_instancerecordsversion = Main.m_instancerecordsversion;
m_fogrange = Main.m_fogrange;
m_billboardgeometry = Main.m_billboardgeometry;
m_glaretexture = Main.m_glaretexture;
m_motionhistory = Main.m_motionhistory;
m_cabfuture = Main.m_cabfuture;
m_cablamplit = Main.m_cablamplit;
m_cabprobelit = Main.m_cabprobelit;
m_previewing = false;
m_cubescenery = false;
m_cubeskipinstance = nullptr;
m_shadowskipvehicle = nullptr;
m_renderme.clear();
// the state of the thread of the main renderer, taken by the thread of the walker when it begins
m_forkmatrices = OpenGLMatrices;
m_forksubmodels = submodel_state{TSubModel::iInstance, TSubModel::iVariantSeed, TSubModel::ReplacableSkinId, TSubModel::iAlpha, TSubModel::fSquareDist, TSubModel::pRoot, TSubModel::pasText};
// the draws: the offsets of the blocks of the pass set as the main renderer has them, the pass sets of the frame, the parts of the
// rings of its own
auto &ctx{*m_context};
auto &draw{*m_draw};
auto const &maindraw{*Main.m_draw};
draw.scene_offset = maindraw.scene_offset;
draw.light_offset = maindraw.light_offset;
draw.cluster_offset = maindraw.cluster_offset;
draw.weather_offset = maindraw.weather_offset;
draw.pass_sets = ctx.scene.pass_sets;
draw.rendering = false;
draw.program = nullptr;
draw.textures.fill(null_handle);
draw.textures_dirty = true;
draw.cull_sets = nullptr;
draw.resume.valid = false;
draw.depthpass = false;
draw.probepass = false;
draw.async = true;
draw.lastprogram = nullptr;
draw.draws_cache.reset(&ctx.scene.draws.ring, vk14::draw_ring_part);
draw.instances_cache.reset(&ctx.scene.instances.ring, vk14::instance_ring_part);
draw.draws_window = {};
draw.instances_window = {};
draw.used_banks.clear();
draw.used_textures.clear();
m_walkfailed = false;
}
void vulkan14_renderer::fork_walk(std::function<void(vulkan14_renderer &)> Job, walk_kind const Kind)
{
auto &ctx{*m_context};
if (false == m_walkwindow)
{
// the pass sets of the frame as the walkers bind them (they don't make them anew), then the walk lock held by the main thread,
// which walks as well from now on
update_pass_sets();
gfx::walk_sync::open();
m_walkwindow = true;
}
auto const index{m_walkjobs++};
while (m_walkers.size() <= index)
{
m_walkers.emplace_back(std::make_unique<vulkan14_renderer>(*this));
}
if (m_walkstreams.size() < ctx.frames.size())
{
m_walkstreams.resize(ctx.frames.size());
}
auto &streams{m_walkstreams[ctx.slot]};
while (streams.size() <= index)
{
streams.emplace_back(std::make_unique<vk14::command_stream>());
}
auto &stream{*streams[index]};
auto &walker{*m_walkers[index]};
// the stream of the frame which used it last was recorded into its command buffers before the slot of the frame came back
stream.clear();
walker.take_state(*this);
walker.m_walkkind = Kind;
walker.m_commands = &stream;
walker.m_draw->commands = &stream;
// the pass goes to the gpu here, with the timestamps the main thread put around it
m_commands->splice(stream);
ctx.walks.add(
[&walker, &stream, job = std::move(Job)]() {
gfx::walk_sync::walk_scope scope;
// the state of the thread of the main renderer at the moment it handed the pass over
OpenGLMatrices = walker.m_forkmatrices;
auto const &submodels{walker.m_forksubmodels};
TSubModel::iInstance = submodels.instance;
TSubModel::iVariantSeed = submodels.variantseed;
TSubModel::ReplacableSkinId = submodels.skins;
TSubModel::iAlpha = submodels.alpha;
TSubModel::fSquareDist = submodels.squaredistance;
TSubModel::pRoot = submodels.root;
TSubModel::pasText = submodels.text;
try
{
job(walker);
}
catch (std::exception const &Exception)
{
ErrorLog(std::string("vulkan: a walker failed to walk its pass, which is left out: ") + Exception.what());
walker.m_walkfailed = true;
}
if (walker.m_walkfailed)
{
// whatever it left half done (a rendering under way) goes
stream.clear();
}
walker.m_commands = nullptr;
walker.m_draw->commands = nullptr;
stream.finish();
},
false);
}
void vulkan14_renderer::join_walks()
{
if (false == m_walkwindow)
{
return;
}
auto &ctx{*m_context};
auto const start{std::chrono::steady_clock::now()};
// the main thread lets go of the walk lock before it waits, so the walkers can make what they need meanwhile
gfx::walk_sync::leave();
ctx.walks.wait_idle();
gfx::walk_sync::close();
m_walkwindow = false;
auto const waited{std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count()};
m_walkwait = (m_walkwait == 0.0 ? waited : m_walkwait * 0.9 + waited * 0.1);
// what the walkers' draws used marked as used, what they did counted
if (m_walkcountsframe != m_framestamp)
{
m_walkcountsframe = m_framestamp;
m_lastwalkcounts = m_walkcounts;
m_walkcounts = {};
}
std::uint32_t lampdraws{0};
std::uint32_t headlightdraws{0};
for (std::size_t index = 0; index < m_walkjobs; ++index)
{
auto &walker{*m_walkers[index]};
auto &draw{*walker.m_draw};
for (auto const bank : draw.used_banks)
{
m_geometry.mark_used(gfx::geometry_handle{bank, 1});
}
if (ctx.textures)
{
for (auto const texture : draw.used_textures)
{
ctx.textures->mark_as_used(texture);
}
}
draw.used_banks.clear();
draw.used_textures.clear();
if (walker.m_walkcountsframe == m_framestamp)
{
m_walkcounts.walked += walker.m_walkcounts.walked;
m_walkcounts.changing += walker.m_walkcounts.changing;
m_walkcounts.reused += walker.m_walkcounts.reused;
m_walkcounts.walktime += walker.m_walkcounts.walktime;
m_walkcounts.ordertime += walker.m_walkcounts.ordertime;
}
auto const drawcalls{static_cast<std::uint32_t>(walker.m_renderpass.draw_stats.drawcalls)};
switch (walker.m_walkkind)
{
case walk_kind::lamp:
lampdraws += drawcalls;
break;
case walk_kind::headlight:
headlightdraws += drawcalls;
break;
default:
break;
}
walker.Settle_Renderme(false);
}
ctx.scene.lamps.add_drawcalls(lampdraws, headlightdraws);
// the vehicles drawn by the colour pass keep what it set, as when the passes come one after another; then what the walks put off
Settle_Renderme(true);
Release_Deferred();
m_walkpasses = m_walkjobs;
m_walkjobs = 0;
}
// the walks of the groups of the cullings split among the helpers (the stage M3, gfx.vulkan.parallelmodels): a few groups are walked by
// the walk itself
bool vulkan14_renderer::walk_groups_parallel(std::size_t const Groups)
{
m_helpgroups = Groups;
return Global.gfx_vulkan_parallelmodels && (m_context != nullptr) && m_context->models.running() && (Groups >= 16) && (false == m_previewing) && (false == m_rtgather);
}
void vulkan14_renderer::take_helper_state(vulkan14_renderer const &Walk)
{
// what the walk of the groups reads: the pass, the model block and the blending the walk is at, its recording of the draws
m_renderpass = Walk.m_renderpass;
m_renderpass.draw_stats = {};
model_ubs = Walk.model_ubs;
m_framestamp = Walk.m_framestamp;
m_instancerecordsversion = Walk.m_instancerecordsversion;
m_fogrange = Walk.m_fogrange;
m_previewing = Walk.m_previewing;
m_motionhistory = Walk.m_motionhistory;
m_cabfuture = Walk.m_cabfuture;
m_invalid_material = Walk.m_invalid_material;
m_rtgather = false;
m_indirectrecording = true;
m_indirectentry = Walk.m_indirectentry;
m_indirectmaterial = Walk.m_indirectmaterial;
m_indirectsubmodel = Walk.m_indirectsubmodel;
m_indirectchanging = false;
m_forkmatrices = OpenGLMatrices;
m_forksubmodels = submodel_state{TSubModel::iInstance, TSubModel::iVariantSeed, TSubModel::ReplacableSkinId, TSubModel::iAlpha, TSubModel::fSquareDist, TSubModel::pRoot, TSubModel::pasText};
auto &draw{*m_draw};
draw.blending = Walk.m_draw->blending;
draw.rendering = false;
draw.depthpass = false;
draw.probepass = false;
draw.used_banks.clear();
draw.used_textures.clear();
}
void vulkan14_renderer::walk_groups_run(std::size_t const Tasks, std::function<void(gfx::scene_walk &Helper, std::size_t const Task)> const &Work)
{
auto &ctx{*m_context};
auto *main{m_main != nullptr ? m_main : this};
// the helpers hold the models they walk (gfx::walk_sync): the window of the walks is opened for them when the main thread walks
// alone
auto const opened{(m_main == nullptr) && (false == gfx::walk_sync::parallel())};
if (opened)
{
gfx::walk_sync::open();
}
while (m_helpers.size() < Tasks)
{
m_helpers.emplace_back(std::make_unique<vulkan14_renderer>(*main));
}
for (std::size_t task = 0; task < Tasks; ++task)
{
m_helpers[task]->take_helper_state(*this);
}
std::mutex mutex;
std::condition_variable finished;
std::size_t left{Tasks};
std::exception_ptr failure;
for (std::size_t task = 0; task < Tasks; ++task)
{
ctx.models.add(
[&, task]() {
{
// covered by the hold of the walk waiting for it
gfx::walk_sync::borrowed_scope scope;
auto &helper{*m_helpers[task]};
OpenGLMatrices = helper.m_forkmatrices;
auto const &submodels{helper.m_forksubmodels};
TSubModel::iInstance = submodels.instance;
TSubModel::iVariantSeed = submodels.variantseed;
TSubModel::ReplacableSkinId = submodels.skins;
TSubModel::iAlpha = submodels.alpha;
TSubModel::fSquareDist = submodels.squaredistance;
TSubModel::pRoot = submodels.root;
TSubModel::pasText = submodels.text;
try
{
Work(helper, task);
}
catch (...)
{
std::lock_guard<std::mutex> lock(mutex);
failure = std::current_exception();
}
}
std::lock_guard<std::mutex> lock(mutex);
if (--left == 0)
{
finished.notify_all();
}
},
false);
}
{
std::unique_lock<std::mutex> lock(mutex);
finished.wait(lock, [&]() { return left == 0; });
}
// the banks the helpers' draws used, marked with those of the walk
for (std::size_t task = 0; task < Tasks; ++task)
{
auto &used{m_helpers[task]->m_draw->used_banks};
for (auto const bank : used)
{
if (m_main != nullptr)
{
m_draw->used_banks.push_back(bank);
}
else
{
m_geometry.mark_used(gfx::geometry_handle{bank, 1});
}
}
used.clear();
}
if (opened)
{
gfx::walk_sync::close();
}
ctx.helped += m_helpgroups;
if (failure)
{
std::rethrow_exception(failure);
}
}

View File

@@ -146,6 +146,8 @@ bool vulkan14_renderer::init_weather(std::string &Reason)
desc.extent = {static_cast<std::uint32_t>(Size), static_cast<std::uint32_t>(Size), Volume ? static_cast<std::uint32_t>(Size) : 1u};
desc.mips = static_cast<std::uint32_t>(std::floor(std::log2(static_cast<float>(Size)))) + 1;
desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
// read by the passes of the weather on the compute queue (the async compute of M6)
desc.shared = true;
Image = std::make_shared<vk14::image>();
if (false == Image->create(ctx.memory, desc, Name))
{
@@ -164,6 +166,7 @@ bool vulkan14_renderer::init_weather(std::string &Reason)
vk14::image_desc desc;
desc.format = VK_FORMAT_R32_SFLOAT;
desc.usage = VK_IMAGE_USAGE_STORAGE_BIT;
desc.shared = true;
weather.depth_dummy = std::make_shared<vk14::image>();
if (false == weather.depth_dummy->create(ctx.memory, desc, "clouds panorama depth"))
{
@@ -223,8 +226,10 @@ void vulkan14_renderer::destroy_weather()
namespace
{
// an image of the weather made anew: the old one goes once the gpu is done with the frames which used it (Value)
std::shared_ptr<vk14::image> weather_image(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::shared_ptr<vk14::image> &Image, VkImageType const Type, VkFormat const Format, glm::ivec3 const &Size, char const *Name)
// an image of the weather made anew: the old one goes once the gpu is done with the frames which used it (Value). Shared: made by the
// passes of the weather on the compute queue (the async compute of M6)
std::shared_ptr<vk14::image> weather_image(vk14::allocator &Memory, vk14::deletion_queue &Deletions, std::uint64_t const Value, std::shared_ptr<vk14::image> &Image, VkImageType const Type, VkFormat const Format, glm::ivec3 const &Size, char const *Name,
bool const Shared = false)
{
if (Image)
{
@@ -236,6 +241,7 @@ std::shared_ptr<vk14::image> weather_image(vk14::allocator &Memory, vk14::deleti
desc.format = Format;
desc.extent = {static_cast<std::uint32_t>(std::max(Size.x, 1)), static_cast<std::uint32_t>(std::max(Size.y, 1)), static_cast<std::uint32_t>(std::max(Size.z, 1))};
desc.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
desc.shared = Shared;
Image = std::make_shared<vk14::image>();
if (false == Image->create(Memory, desc, Name))
{
@@ -251,7 +257,7 @@ void vulkan14_renderer::make_cloud_noise()
{
auto &ctx{*m_context};
auto &weather{ctx.scene.weather};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const device{ctx.device.handle()};
ctx.passes.begin_pass("clouds noise", {vk14::use_of(*weather.shape_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write), vk14::use_of(*weather.detail_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write),
vk14::use_of(*weather.coverage_noise, VK_IMAGE_LAYOUT_GENERAL, compute_stage, storage_write)});
@@ -364,7 +370,7 @@ void vulkan14_renderer::update_weather()
auto &ctx{*m_context};
auto &scene{ctx.scene};
auto &weather{scene.weather};
auto const commands{ctx.commands};
auto const commands{m_commands};
if (false == weather.noise_made)
{
make_cloud_noise();
@@ -376,7 +382,7 @@ void vulkan14_renderer::update_weather()
if (weather.shadow_size != Global.gfx_clouds_shadows_size)
{
weather.shadow_size = Global.gfx_clouds_shadows_size;
weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.shadow_map, VK_IMAGE_TYPE_2D, VK_FORMAT_R16_SFLOAT, glm::ivec3{weather.shadow_size, weather.shadow_size, 1}, "cloud shadow map");
weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.shadow_map, VK_IMAGE_TYPE_2D, VK_FORMAT_R16_SFLOAT, glm::ivec3{weather.shadow_size, weather.shadow_size, 1}, "cloud shadow map", true);
if (weather.shadow_map)
{
// read before it's first drawn (the pass sets hold it)
@@ -430,7 +436,7 @@ void vulkan14_renderer::update_weather()
if ((weather.panorama_size != level.panorama) || (false == bool(weather.panorama)))
{
weather.panorama_size = level.panorama;
weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.panorama, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{weather.panorama_size, 1}, "clouds panorama");
weather_image(ctx.memory, ctx.deletions, ctx.uploads.current_value(), weather.panorama, VK_IMAGE_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT, glm::ivec3{weather.panorama_size, 1}, "clouds panorama", true);
m_weatherstate.panorama_resized();
}
if (weather.panorama)
@@ -460,7 +466,7 @@ void vulkan14_renderer::render_weather_view(glm::mat4 const &Projection, glm::ma
{
auto &ctx{*m_context};
auto &weather{ctx.scene.weather};
auto const commands{ctx.commands};
auto const commands{m_commands};
auto const level{gfx::weather_state::quality(Global.gfx_clouds, Global.gfx_fog_quality)};
// the images of the clouds of the main view and of the fog volume, for the size and the quality
auto const cloudsize{glm::max((Size + level.divisor - 1) / level.divisor, glm::ivec2{1})};