GPU: Framebuffer: Add multi viewport support

This add the possibility to define different
viewports inside a single framebuffer and
let the vertex shader decide which viewport
to render to.

This only contain the GL and VK implementation.
The Vulkan implementation works but still
has a validation error related to shader features
and extension. The test passes nonetheless.

Pull Request: https://projects.blender.org/blender/blender/pulls/110923
This commit is contained in:
Clément Foucault
2023-08-08 17:12:49 +02:00
committed by Clément Foucault
parent e1b1b2a8b4
commit 983ff8e616
17 changed files with 248 additions and 62 deletions

View File

@@ -219,6 +219,7 @@ class GHOST_DeviceVK {
device_features.dualSrcBlend = VK_TRUE;
device_features.logicOp = VK_TRUE;
device_features.imageCubeArray = VK_TRUE;
device_features.multiViewport = VK_TRUE;
#endif
VkDeviceCreateInfo device_create_info = {};

View File

@@ -571,6 +571,7 @@ set(GLSL_SRC_TEST
tests/shaders/gpu_compute_ssbo_test.glsl
tests/shaders/gpu_compute_vbo_test.glsl
tests/shaders/gpu_compute_dummy_test.glsl
tests/shaders/gpu_framebuffer_layer_viewport_test.glsl
tests/shaders/gpu_push_constants_test.glsl
)

View File

@@ -35,6 +35,9 @@ typedef enum eGPUFrameBufferBits {
ENUM_OPERATORS(eGPUFrameBufferBits, GPU_STENCIL_BIT)
/* Guaranteed by the spec and is never greater than 16 on any hardware or implementation. */
#define GPU_MAX_VIEWPORTS 16
#ifdef __cplusplus
extern "C" {
#endif
@@ -340,10 +343,23 @@ void GPU_framebuffer_default_size(GPUFrameBuffer *framebuffer, int width, int he
* or when binding the frame-buffer after modifying its attachments.
*
* \note Viewport and scissor size is stored per frame-buffer.
* \note Setting a singular viewport will only change the state of the first viewport.
* \note Must be called after first bind.
*/
void GPU_framebuffer_viewport_set(
GPUFrameBuffer *framebuffer, int x, int y, int width, int height);
/**
* Similar to `GPU_framebuffer_viewport_set()` but specify the bounds of all 16 viewports.
* By default geometry renders only to the first viewport. That can be changed by setting
* `gpu_ViewportIndex` in the vertex.
*
* \note Viewport and scissor size is stored per frame-buffer.
* \note Must be called after first bind.
*/
void GPU_framebuffer_multi_viewports_set(GPUFrameBuffer *gpu_fb,
const int viewport_rects[GPU_MAX_VIEWPORTS][4]);
/**
* Return the viewport offset and size in a int quadruple: (x, y, width, height).
* \note Viewport and scissor size is stored per frame-buffer.

View File

@@ -391,6 +391,12 @@ void GPU_framebuffer_viewport_set(GPUFrameBuffer *gpu_fb, int x, int y, int widt
unwrap(gpu_fb)->viewport_set(viewport_rect);
}
void GPU_framebuffer_multi_viewports_set(GPUFrameBuffer *gpu_fb,
const int viewport_rects[GPU_MAX_VIEWPORTS][4])
{
unwrap(gpu_fb)->viewport_multi_set(viewport_rects);
}
void GPU_framebuffer_viewport_get(GPUFrameBuffer *gpu_fb, int r_viewport[4])
{
unwrap(gpu_fb)->viewport_get(r_viewport);

View File

@@ -81,8 +81,9 @@ class FrameBuffer {
/** Debug name. */
char name_[DEBUG_NAME_LEN];
/** Frame-buffer state. */
int viewport_[4] = {0};
int viewport_[GPU_MAX_VIEWPORTS][4] = {{0}};
int scissor_[4] = {0};
bool multi_viewport_ = false;
bool scissor_test_ = false;
bool dirty_state_ = true;
@@ -157,10 +158,22 @@ class FrameBuffer {
inline void viewport_set(const int viewport[4])
{
if (!equals_v4v4_int(viewport_, viewport)) {
copy_v4_v4_int(viewport_, viewport);
if (!equals_v4v4_int(viewport_[0], viewport)) {
copy_v4_v4_int(viewport_[0], viewport);
dirty_state_ = true;
}
multi_viewport_ = false;
}
inline void viewport_multi_set(const int viewports[GPU_MAX_VIEWPORTS][4])
{
for (size_t i = 0; i < GPU_MAX_VIEWPORTS; i++) {
if (!equals_v4v4_int(viewport_[i], viewports[i])) {
copy_v4_v4_int(viewport_[i], viewports[i]);
dirty_state_ = true;
}
}
multi_viewport_ = true;
}
inline void scissor_set(const int scissor[4])
@@ -178,7 +191,7 @@ class FrameBuffer {
inline void viewport_get(int r_viewport[4]) const
{
copy_v4_v4_int(r_viewport, viewport_);
copy_v4_v4_int(r_viewport, viewport_[0]);
}
inline void scissor_get(int r_scissor[4]) const

View File

@@ -178,6 +178,11 @@ enum class BuiltinBits {
VERTEX_ID = (1 << 14),
WORK_GROUP_ID = (1 << 15),
WORK_GROUP_SIZE = (1 << 16),
/**
* Allow setting the target viewport when using multi viewport feature.
* \note Emulated through geometry shader on older hardware.
*/
VIEWPORT_INDEX = (1 << 17),
/* Not a builtin but a flag we use to tag shaders that use the debug features. */
USE_DEBUG_DRAW = (1 << 29),

View File

@@ -780,8 +780,8 @@ void MTLFrameBuffer::apply_state()
/* Ensure viewport has been set. NOTE: This should no longer happen, but kept for safety to
* track bugs. If viewport size is zero, use framebuffer size. */
int viewport_w = viewport_[2];
int viewport_h = viewport_[3];
int viewport_w = viewport_[0][2];
int viewport_h = viewport_[0][3];
if (viewport_w == 0 || viewport_h == 0) {
MTL_LOG_WARNING("Viewport had width and height of (0,0) -- Updating -- DEBUG Safety check");
viewport_w = default_width_;
@@ -789,7 +789,7 @@ void MTLFrameBuffer::apply_state()
}
/* Update Context State. */
mtl_ctx->set_viewport(viewport_[0], viewport_[1], viewport_w, viewport_h);
mtl_ctx->set_viewport(viewport_[0][0], viewport_[0][1], viewport_w, viewport_h);
mtl_ctx->set_scissor(scissor_[0], scissor_[1], scissor_[2], scissor_[3]);
mtl_ctx->set_scissor_enabled(scissor_test_);

View File

@@ -592,7 +592,8 @@ void GLBackend::capabilities_init()
GLContext::explicit_location_support = epoxy_gl_version() >= 43;
GLContext::geometry_shader_invocations = epoxy_has_gl_extension("GL_ARB_gpu_shader5");
GLContext::fixed_restart_index_support = epoxy_has_gl_extension("GL_ARB_ES3_compatibility");
GLContext::layered_rendering_support = epoxy_has_gl_extension("GL_AMD_vertex_shader_layer");
GLContext::layered_rendering_support = epoxy_has_gl_extension(
"GL_ARB_shader_viewport_layer_array");
GLContext::native_barycentric_support = epoxy_has_gl_extension(
"GL_AMD_shader_explicit_vertex_parameter");
GLContext::multi_bind_support = GLContext::multi_bind_image_support = epoxy_has_gl_extension(

View File

@@ -43,10 +43,10 @@ GLFrameBuffer::GLFrameBuffer(
height_ = h;
srgb_ = false;
viewport_[0] = scissor_[0] = 0;
viewport_[1] = scissor_[1] = 0;
viewport_[2] = scissor_[2] = w;
viewport_[3] = scissor_[3] = h;
viewport_[0][0] = scissor_[0] = 0;
viewport_[0][1] = scissor_[1] = 0;
viewport_[0][2] = scissor_[2] = w;
viewport_[0][3] = scissor_[3] = h;
if (fbo_id_) {
debug::object_label(GL_FRAMEBUFFER, fbo_id_, name_);
@@ -230,7 +230,20 @@ void GLFrameBuffer::apply_state()
return;
}
glViewport(UNPACK4(viewport_));
if (multi_viewport_ == false) {
glViewport(UNPACK4(viewport_[0]));
}
else {
/* Great API you have there! You have to convert to float values for setting int viewport
* values. **Audible Facepalm** */
float viewports_f[GPU_MAX_VIEWPORTS][4];
for (int i = 0; i < GPU_MAX_VIEWPORTS; i++) {
for (int j = 0; j < 4; j++) {
viewports_f[i][j] = viewport_[i][j];
}
}
glViewportArrayv(0, GPU_MAX_VIEWPORTS, viewports_f[0]);
}
glScissor(UNPACK4(scissor_));
if (scissor_test_) {

View File

@@ -551,6 +551,10 @@ std::string GLShader::vertex_interface_declare(const ShaderCreateInfo &info) con
if (!GLContext::layered_rendering_support && bool(info.builtins_ & BuiltinBits::LAYER)) {
ss << "out int gpu_Layer;\n";
}
if (!GLContext::layered_rendering_support && bool(info.builtins_ & BuiltinBits::VIEWPORT_INDEX))
{
ss << "out int gpu_ViewportIndex;\n";
}
if (bool(info.builtins_ & BuiltinBits::BARYCENTRIC_COORD)) {
if (!GLContext::native_barycentric_support) {
/* Disabled or unsupported. */
@@ -584,6 +588,13 @@ std::string GLShader::fragment_interface_declare(const ShaderCreateInfo &info) c
for (const StageInterfaceInfo *iface : in_interfaces) {
print_interface(ss, "in", *iface);
}
if (!GLContext::layered_rendering_support && bool(info.builtins_ & BuiltinBits::LAYER)) {
ss << "#define gpu_Layer gl_Layer\n";
}
if (!GLContext::layered_rendering_support && bool(info.builtins_ & BuiltinBits::VIEWPORT_INDEX))
{
ss << "#define gpu_ViewportIndex gl_ViewportIndex\n";
}
if (bool(info.builtins_ & BuiltinBits::BARYCENTRIC_COORD)) {
if (!GLContext::native_barycentric_support) {
ss << "flat in vec4 gpu_pos[3];\n";
@@ -736,6 +747,8 @@ std::string GLShader::workaround_geometry_shader_source_create(
const bool do_layer_workaround = !GLContext::layered_rendering_support &&
bool(info.builtins_ & BuiltinBits::LAYER);
const bool do_viewport_workaround = !GLContext::layered_rendering_support &&
bool(info.builtins_ & BuiltinBits::VIEWPORT_INDEX);
const bool do_barycentric_workaround = !GLContext::native_barycentric_support &&
bool(info.builtins_ & BuiltinBits::BARYCENTRIC_COORD);
@@ -752,6 +765,9 @@ std::string GLShader::workaround_geometry_shader_source_create(
if (do_layer_workaround) {
ss << "in int gpu_Layer[];\n";
}
if (do_viewport_workaround) {
ss << "in int gpu_ViewportIndex[];\n";
}
if (do_barycentric_workaround) {
ss << "flat out vec4 gpu_pos[3];\n";
ss << "smooth out vec3 gpu_BaryCoord;\n";
@@ -764,6 +780,9 @@ std::string GLShader::workaround_geometry_shader_source_create(
if (do_layer_workaround) {
ss << " gl_Layer = gpu_Layer[0];\n";
}
if (do_viewport_workaround) {
ss << " gl_ViewportIndex = gpu_ViewportIndex[0];\n";
}
if (do_barycentric_workaround) {
ss << " gpu_pos[0] = gl_in[0].gl_Position;\n";
ss << " gpu_pos[1] = gl_in[1].gl_Position;\n";
@@ -796,6 +815,9 @@ bool GLShader::do_geometry_shader_injection(const shader::ShaderCreateInfo *info
if (!GLContext::layered_rendering_support && bool(builtins & BuiltinBits::LAYER)) {
return true;
}
if (!GLContext::layered_rendering_support && bool(builtins & BuiltinBits::VIEWPORT_INDEX)) {
return true;
}
return false;
}
@@ -853,8 +875,9 @@ static char *glsl_patch_default_get()
STR_CONCAT(patch, slen, "#extension GL_ARB_shading_language_420pack: enable\n");
}
if (GLContext::layered_rendering_support) {
STR_CONCAT(patch, slen, "#extension GL_AMD_vertex_shader_layer: enable\n");
STR_CONCAT(patch, slen, "#extension GL_ARB_shader_viewport_layer_array: enable\n");
STR_CONCAT(patch, slen, "#define gpu_Layer gl_Layer\n");
STR_CONCAT(patch, slen, "#define gpu_ViewportIndex gl_ViewportIndex\n");
}
if (GLContext::native_barycentric_support) {
STR_CONCAT(patch, slen, "#extension GL_AMD_shader_explicit_vertex_parameter: enable\n");

View File

@@ -6,10 +6,13 @@
#include "GPU_context.h"
#include "GPU_framebuffer.h"
#include "GPU_shader.h"
#include "gpu_testing.hh"
#include "BLI_math_vector.hh"
#include "gpu_shader_create_info.hh"
namespace blender::gpu::tests {
static void test_framebuffer_clear_color_single_attachment()
@@ -248,4 +251,77 @@ static void test_framebuffer_cube()
}
GPU_TEST(framebuffer_cube)
/* Effectively tests the same way EEVEE-Next shadows are rendered. */
static void test_framebuffer_multi_viewport()
{
using namespace gpu::shader;
GPU_render_begin();
const int2 size(4, 4);
const int layers = 256;
eGPUTextureUsage usage = GPU_TEXTURE_USAGE_ATTACHMENT | GPU_TEXTURE_USAGE_HOST_READ;
GPUTexture *texture = GPU_texture_create_2d_array(
__func__, UNPACK2(size), layers, 1, GPU_RG32I, usage, nullptr);
GPUFrameBuffer *framebuffer = GPU_framebuffer_create(__func__);
GPU_framebuffer_ensure_config(&framebuffer,
{GPU_ATTACHMENT_NONE, GPU_ATTACHMENT_TEXTURE(texture)});
GPU_framebuffer_bind(framebuffer);
int viewport_rects[16][4];
for (int i = 0; i < 16; i++) {
viewport_rects[i][0] = i % 4;
viewport_rects[i][1] = i / 4;
viewport_rects[i][2] = 1;
viewport_rects[i][3] = 1;
}
GPU_framebuffer_multi_viewports_set(framebuffer, viewport_rects);
const float4 clear_color(0.0f);
GPU_framebuffer_clear_color(framebuffer, clear_color);
ShaderCreateInfo create_info("");
create_info.vertex_source("gpu_framebuffer_layer_viewport_test.glsl");
create_info.fragment_source("gpu_framebuffer_layer_viewport_test.glsl");
create_info.builtins(BuiltinBits::VIEWPORT_INDEX | BuiltinBits::LAYER);
create_info.fragment_out(0, Type::IVEC2, "out_value");
GPUShader *shader = GPU_shader_create_from_info(
reinterpret_cast<GPUShaderCreateInfo *>(&create_info));
/* TODO(fclem): remove this boilerplate. */
GPUVertFormat format{};
GPU_vertformat_attr_add(&format, "dummy", GPU_COMP_U32, 1, GPU_FETCH_INT);
GPUVertBuf *verts = GPU_vertbuf_create_with_format(&format);
GPU_vertbuf_data_alloc(verts, 3);
GPUBatch *batch = GPU_batch_create_ex(GPU_PRIM_TRIS, verts, nullptr, GPU_BATCH_OWNS_VBO);
GPU_batch_set_shader(batch, shader);
int tri_count = size.x * size.y * layers;
GPU_batch_draw_advanced(batch, 0, tri_count * 3, 0, 1);
GPU_batch_discard(batch);
GPU_finish();
int2 *read_data = static_cast<int2 *>(GPU_texture_read(texture, GPU_DATA_INT, 0));
for (auto layer : IndexRange(layers)) {
for (auto viewport : IndexRange(16)) {
int2 expected_color(layer, viewport);
int2 pixel_color = read_data[viewport + layer * 16];
EXPECT_EQ(pixel_color, expected_color);
}
}
MEM_freeN(read_data);
GPU_framebuffer_free(framebuffer);
GPU_texture_free(texture);
GPU_shader_free(shader);
GPU_render_end();
}
GPU_TEST(framebuffer_multi_viewport)
} // namespace blender::gpu::tests

View File

@@ -0,0 +1,23 @@
#ifdef GPU_VERTEX_SHADER
void main()
{
/* Fullscreen triangle. */
int v = gl_VertexID % 3;
float x = -1.0 + float((v & 1) << 2);
float y = -1.0 + float((v & 2) << 1);
/* NOTE: Make it cover more than one viewport to test default scissors. */
gl_Position = vec4(x * 2.0, y * 2.0, 1.0, 1.0);
int index = gl_VertexID / 3;
gpu_ViewportIndex = index % 16;
gpu_Layer = index / 16;
}
#endif
#ifdef GPU_FRAGMENT_SHADER
void main()
{
out_value = ivec2(gpu_Layer, gpu_ViewportIndex);
}
#endif

View File

@@ -376,7 +376,7 @@ void VKCommandBuffer::ensure_active_framebuffer()
render_pass_begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
render_pass_begin_info.renderPass = state.framebuffer_->vk_render_pass_get();
render_pass_begin_info.framebuffer = state.framebuffer_->vk_framebuffer_get();
render_pass_begin_info.renderArea = state.framebuffer_->vk_render_area_get();
render_pass_begin_info.renderArea = state.framebuffer_->vk_render_areas_get()[0];
/* We don't use clear ops, but vulkan wants to have at least one. */
VkClearValue clear_value = {};
render_pass_begin_info.clearValueCount = 1;

View File

@@ -71,52 +71,53 @@ void VKFrameBuffer::bind(bool /*enabled_srgb*/)
context.activate_framebuffer(*this);
}
VkViewport VKFrameBuffer::vk_viewport_get() const
Array<VkViewport, 16> VKFrameBuffer::vk_viewports_get() const
{
VkViewport viewport;
int viewport_rect[4];
viewport_get(viewport_rect);
Array<VkViewport, 16> viewports(this->multi_viewport_ ? GPU_MAX_VIEWPORTS : 1);
viewport.x = viewport_rect[0];
viewport.y = viewport_rect[1];
viewport.width = viewport_rect[2];
viewport.height = viewport_rect[3];
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
/*
* Vulkan has origin to the top left, Blender bottom left. We counteract this by using a negative
* viewport when flip_viewport_ is set. This flips the viewport making any draw/blit use the
* correct orientation.
*/
if (flip_viewport_) {
viewport.y = height_ - viewport_rect[1];
viewport.height = -viewport_rect[3];
int index = 0;
for (VkViewport &viewport : viewports) {
viewport.x = viewport_[index][0];
viewport.y = viewport_[index][1];
viewport.width = viewport_[index][2];
viewport.height = viewport_[index][3];
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
/*
* Vulkan has origin to the top left, Blender bottom left. We counteract this by using a
* negative viewport when flip_viewport_ is set. This flips the viewport making any draw/blit
* use the correct orientation.
*/
if (flip_viewport_) {
viewport.y = height_ - viewport_[index][1];
viewport.height = -viewport_[index][3];
}
index++;
}
return viewport;
return viewports;
}
VkRect2D VKFrameBuffer::vk_render_area_get() const
Array<VkRect2D, 16> VKFrameBuffer::vk_render_areas_get() const
{
VkRect2D render_area = {};
Array<VkRect2D, 16> render_areas(this->multi_viewport_ ? GPU_MAX_VIEWPORTS : 1);
if (scissor_test_get()) {
int scissor_rect[4];
scissor_get(scissor_rect);
render_area.offset.x = scissor_rect[0];
render_area.offset.y = scissor_rect[1];
render_area.extent.width = scissor_rect[2];
render_area.extent.height = scissor_rect[3];
for (VkRect2D &render_area : render_areas) {
if (scissor_test_get()) {
int scissor_rect[4];
scissor_get(scissor_rect);
render_area.offset.x = scissor_rect[0];
render_area.offset.y = scissor_rect[1];
render_area.extent.width = scissor_rect[2];
render_area.extent.height = scissor_rect[3];
}
else {
render_area.offset.x = 0;
render_area.offset.y = 0;
render_area.extent.width = width_;
render_area.extent.height = height_;
}
}
else {
render_area.offset.x = 0;
render_area.offset.y = 0;
render_area.extent.width = width_;
render_area.extent.height = height_;
}
return render_area;
return render_areas;
}
bool VKFrameBuffer::check(char /*err_out*/[256])
@@ -170,7 +171,7 @@ void VKFrameBuffer::clear(const Vector<VkClearAttachment> &attachments) const
return;
}
VkClearRect clear_rect = {};
clear_rect.rect = vk_render_area_get();
clear_rect.rect = vk_render_areas_get()[0];
clear_rect.baseArrayLayer = 0;
clear_rect.layerCount = 1;

View File

@@ -8,6 +8,7 @@
#pragma once
#include "BLI_array.hh"
#include "BLI_math_vector.hh"
#include "BLI_span.hh"
#include "BLI_vector.hh"
@@ -107,8 +108,8 @@ class VKFrameBuffer : public FrameBuffer {
BLI_assert(vk_render_pass_ != VK_NULL_HANDLE);
return vk_render_pass_;
}
VkViewport vk_viewport_get() const;
VkRect2D vk_render_area_get() const;
Array<VkViewport, 16> vk_viewports_get() const;
Array<VkRect2D, 16> vk_render_areas_get() const;
VkImage vk_image_get() const
{
BLI_assert(vk_image_ != VK_NULL_HANDLE);

View File

@@ -164,12 +164,12 @@ void VKPipeline::finalize(VKContext &context,
/* Viewport state. */
VkPipelineViewportStateCreateInfo viewport_state = {};
viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
VkViewport viewport = framebuffer.vk_viewport_get();
viewport_state.pViewports = &viewport;
viewport_state.viewportCount = 1;
VkRect2D scissor = framebuffer.vk_render_area_get();
viewport_state.pScissors = &scissor;
viewport_state.scissorCount = 1;
Array<VkViewport, 16> viewports = framebuffer.vk_viewports_get();
viewport_state.pViewports = &viewports[0];
viewport_state.viewportCount = viewports.size();
Array<VkRect2D, 16> scissors = framebuffer.vk_render_areas_get();
viewport_state.pScissors = &scissors[0];
viewport_state.scissorCount = scissors.size();
pipeline_create_info.pViewportState = &viewport_state;
/* Multi-sample state. */

View File

@@ -500,6 +500,11 @@ static char *glsl_patch_get()
STR_CONCAT(patch, slen, "#define gl_InstanceID gpu_InstanceIndex\n");
/* TODO(fclem): This creates a validation error and should be already part of Vulkan 1.2. */
STR_CONCAT(patch, slen, "#extension GL_ARB_shader_viewport_layer_array: enable\n");
STR_CONCAT(patch, slen, "#define gpu_Layer gl_Layer\n");
STR_CONCAT(patch, slen, "#define gpu_ViewportIndex gl_ViewportIndex\n");
STR_CONCAT(patch, slen, "#define DFDX_SIGN 1.0\n");
STR_CONCAT(patch, slen, "#define DFDY_SIGN 1.0\n");
@@ -526,6 +531,7 @@ Vector<uint32_t> VKShader::compile_glsl_to_spirv(Span<const char *> sources,
shaderc::Compiler &compiler = backend.get_shaderc_compiler();
shaderc::CompileOptions options;
options.SetOptimizationLevel(shaderc_optimization_level_performance);
options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);
if (G.debug & G_DEBUG_GPU_RENDERDOC) {
options.SetOptimizationLevel(shaderc_optimization_level_zero);
options.SetGenerateDebugInfo();