Vulkan: Refactor shader interface. #105073
|
@ -225,7 +225,7 @@ static void test_gpu_shader_compute_ssbo()
|
||||||
EXPECT_NE(shader, nullptr);
|
EXPECT_NE(shader, nullptr);
|
||||||
GPU_shader_bind(shader);
|
GPU_shader_bind(shader);
|
||||||
|
|
||||||
/* Construct IBO. */
|
/* Construct SSBO. */
|
||||||
GPUStorageBuf *ssbo = GPU_storagebuf_create_ex(
|
GPUStorageBuf *ssbo = GPU_storagebuf_create_ex(
|
||||||
SIZE * sizeof(uint32_t), nullptr, GPU_USAGE_DEVICE_ONLY, __func__);
|
SIZE * sizeof(uint32_t), nullptr, GPU_USAGE_DEVICE_ONLY, __func__);
|
||||||
GPU_storagebuf_bind(ssbo, GPU_shader_get_ssbo_binding(shader, "data_out"));
|
GPU_storagebuf_bind(ssbo, GPU_shader_get_ssbo_binding(shader, "data_out"));
|
||||||
|
|
|
@ -119,6 +119,10 @@ void VKDescriptorSet::update(VkDevice vk_device)
|
||||||
descriptor_writes.append(write_descriptor);
|
descriptor_writes.append(write_descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BLI_assert_msg(image_infos.size() + buffer_infos.size() == descriptor_writes.size(),
|
||||||
|
"Not all changes have been converted to a write descriptor. Check "
|
||||||
|
"`Binding::is_buffer` and `Binding::is_image`.");
|
||||||
|
|
||||||
vkUpdateDescriptorSets(
|
vkUpdateDescriptorSets(
|
||||||
vk_device, descriptor_writes.size(), descriptor_writes.data(), 0, nullptr);
|
vk_device, descriptor_writes.size(), descriptor_writes.data(), 0, nullptr);
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,11 @@
|
||||||
#include "vk_common.hh"
|
#include "vk_common.hh"
|
||||||
|
|
||||||
namespace blender::gpu {
|
namespace blender::gpu {
|
||||||
class VKStorageBuffer;
|
|
||||||
class VKVertexBuffer;
|
|
||||||
class VKIndexBuffer;
|
class VKIndexBuffer;
|
||||||
|
class VKShaderInterface;
|
||||||
|
class VKStorageBuffer;
|
||||||
class VKTexture;
|
class VKTexture;
|
||||||
|
class VKVertexBuffer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In vulkan shader resources (images and buffers) are grouped in descriptor sets.
|
* In vulkan shader resources (images and buffers) are grouped in descriptor sets.
|
||||||
|
@ -26,7 +27,7 @@ class VKTexture;
|
||||||
* The resources inside a descriptor set can be updated and bound per set.
|
* The resources inside a descriptor set can be updated and bound per set.
|
||||||
*
|
*
|
||||||
* Currently Blender only supports a single descriptor set per shader, but it is planned to be able
|
* Currently Blender only supports a single descriptor set per shader, but it is planned to be able
|
||||||
* to use 2 descriptor sets per shader. Only for each #blender::gpu::shader::Frequency.
|
* to use 2 descriptor sets per shader. One for each #blender::gpu::shader::Frequency.
|
||||||
*/
|
*/
|
||||||
class VKDescriptorSet : NonCopyable {
|
class VKDescriptorSet : NonCopyable {
|
||||||
struct Binding;
|
struct Binding;
|
||||||
|
@ -50,9 +51,12 @@ class VKDescriptorSet : NonCopyable {
|
||||||
*/
|
*/
|
||||||
uint32_t binding;
|
uint32_t binding;
|
||||||
|
|
||||||
Location() = default;
|
Location(uint32_t binding) : binding(binding)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
Location() = default;
|
||||||
Location(const ShaderInput *shader_input) : binding(shader_input->location)
|
Location(const ShaderInput *shader_input) : binding(shader_input->location)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -68,6 +72,7 @@ class VKDescriptorSet : NonCopyable {
|
||||||
}
|
}
|
||||||
|
|
||||||
friend struct Binding;
|
friend struct Binding;
|
||||||
|
friend class VKShaderInterface;
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -24,9 +24,9 @@ void VKIndexBuffer::bind_as_ssbo(uint binding)
|
||||||
|
|
||||||
VKShader *shader = static_cast<VKShader *>(context.shader);
|
VKShader *shader = static_cast<VKShader *>(context.shader);
|
||||||
const VKShaderInterface &shader_interface = shader->interface_get();
|
const VKShaderInterface &shader_interface = shader->interface_get();
|
||||||
const ShaderInput *shader_input = shader_interface.shader_input_get(
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(
|
||||||
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, binding);
|
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, binding);
|
||||||
shader->pipeline_get().descriptor_set_get().bind_as_ssbo(*this, shader_input);
|
shader->pipeline_get().descriptor_set_get().bind_as_ssbo(*this, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VKIndexBuffer::read(uint32_t *data) const
|
void VKIndexBuffer::read(uint32_t *data) const
|
||||||
|
|
|
@ -326,10 +326,10 @@ static std::ostream &print_qualifier(std::ostream &os, const Qualifier &qualifie
|
||||||
}
|
}
|
||||||
|
|
||||||
static void print_resource(std::ostream &os,
|
static void print_resource(std::ostream &os,
|
||||||
const ShaderInput &shader_input,
|
const VKDescriptorSet::Location location,
|
||||||
const ShaderCreateInfo::Resource &res)
|
const ShaderCreateInfo::Resource &res)
|
||||||
{
|
{
|
||||||
os << "layout(binding = " << shader_input.location;
|
os << "layout(binding = " << static_cast<uint32_t>(location);
|
||||||
if (res.bind_type == ShaderCreateInfo::Resource::BindType::IMAGE) {
|
if (res.bind_type == ShaderCreateInfo::Resource::BindType::IMAGE) {
|
||||||
os << ", " << to_string(res.image.format);
|
os << ", " << to_string(res.image.format);
|
||||||
}
|
}
|
||||||
|
@ -379,12 +379,8 @@ static void print_resource(std::ostream &os,
|
||||||
const VKShaderInterface &shader_interface,
|
const VKShaderInterface &shader_interface,
|
||||||
const ShaderCreateInfo::Resource &res)
|
const ShaderCreateInfo::Resource &res)
|
||||||
{
|
{
|
||||||
const ShaderInput *shader_input = shader_interface.shader_input_get(res);
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(res);
|
||||||
if (shader_input == nullptr) {
|
print_resource(os, location, res);
|
||||||
BLI_assert_msg(shader_input, "Cannot find shader input for resource");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
print_resource(os, *shader_input, res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void print_resource_alias(std::ostream &os, const ShaderCreateInfo::Resource &res)
|
static void print_resource_alias(std::ostream &os, const ShaderCreateInfo::Resource &res)
|
||||||
|
@ -860,10 +856,10 @@ static VkDescriptorType descriptor_type(const shader::ShaderCreateInfo::Resource
|
||||||
}
|
}
|
||||||
|
|
||||||
static VkDescriptorSetLayoutBinding create_descriptor_set_layout_binding(
|
static VkDescriptorSetLayoutBinding create_descriptor_set_layout_binding(
|
||||||
const ShaderInput &shader_input, const shader::ShaderCreateInfo::Resource &resource)
|
const VKDescriptorSet::Location location, const shader::ShaderCreateInfo::Resource &resource)
|
||||||
{
|
{
|
||||||
VkDescriptorSetLayoutBinding binding = {};
|
VkDescriptorSetLayoutBinding binding = {};
|
||||||
binding.binding = shader_input.location;
|
binding.binding = location;
|
||||||
binding.descriptorType = descriptor_type(resource);
|
binding.descriptorType = descriptor_type(resource);
|
||||||
binding.descriptorCount = 1;
|
binding.descriptorCount = 1;
|
||||||
binding.stageFlags = VK_SHADER_STAGE_ALL;
|
binding.stageFlags = VK_SHADER_STAGE_ALL;
|
||||||
|
@ -878,13 +874,8 @@ static void add_descriptor_set_layout_bindings(
|
||||||
Vector<VkDescriptorSetLayoutBinding> &r_bindings)
|
Vector<VkDescriptorSetLayoutBinding> &r_bindings)
|
||||||
{
|
{
|
||||||
for (const shader::ShaderCreateInfo::Resource &resource : resources) {
|
for (const shader::ShaderCreateInfo::Resource &resource : resources) {
|
||||||
const ShaderInput *shader_input = interface.shader_input_get(resource);
|
const VKDescriptorSet::Location location = interface.descriptor_set_location(resource);
|
||||||
if (shader_input == nullptr) {
|
r_bindings.append(create_descriptor_set_layout_binding(location, resource));
|
||||||
BLI_assert_msg(shader_input, "Cannot find shader input for resource.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
r_bindings.append(create_descriptor_set_layout_binding(*shader_input, resource));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1033,12 +1024,6 @@ std::string VKShader::vertex_interface_declare(const shader::ShaderCreateInfo &i
|
||||||
ss << "layout(location = " << attr.index << ") ";
|
ss << "layout(location = " << attr.index << ") ";
|
||||||
ss << "in " << to_string(attr.type) << " " << attr.name << ";\n";
|
ss << "in " << to_string(attr.type) << " " << attr.name << ";\n";
|
||||||
}
|
}
|
||||||
/* NOTE(D4490): Fix a bug where shader without any vertex attributes do not behave correctly.
|
|
||||||
*/
|
|
||||||
if (GPU_type_matches_ex(GPU_DEVICE_APPLE, GPU_OS_MAC, GPU_DRIVER_ANY, GPU_BACKEND_OPENGL) &&
|
|
||||||
info.vertex_inputs_.is_empty()) {
|
|
||||||
ss << "in float gpu_dummy_workaround;\n";
|
|
||||||
}
|
|
||||||
ss << "\n/* Interfaces. */\n";
|
ss << "\n/* Interfaces. */\n";
|
||||||
int location = 0;
|
int location = 0;
|
||||||
for (const StageInterfaceInfo *iface : info.vertex_out_interfaces_) {
|
for (const StageInterfaceInfo *iface : info.vertex_out_interfaces_) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ void VKShaderInterface::init(const shader::ShaderCreateInfo &info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Make sure that the image slots don't overlap with the sampler slots.*/
|
/* Make sure that the image slots don't overlap with the sampler slots.*/
|
||||||
image_offset_ += 1;
|
image_offset_++;
|
||||||
|
|
||||||
int32_t input_tot_len = ubo_len_ + uniform_len_ + ssbo_len_;
|
int32_t input_tot_len = ubo_len_ + uniform_len_ + ssbo_len_;
|
||||||
inputs_ = static_cast<ShaderInput *>(
|
inputs_ = static_cast<ShaderInput *>(
|
||||||
|
@ -51,14 +51,11 @@ void VKShaderInterface::init(const shader::ShaderCreateInfo &info)
|
||||||
name_buffer_ = (char *)MEM_mallocN(info.interface_names_size_, "name_buffer");
|
name_buffer_ = (char *)MEM_mallocN(info.interface_names_size_, "name_buffer");
|
||||||
uint32_t name_buffer_offset = 0;
|
uint32_t name_buffer_offset = 0;
|
||||||
|
|
||||||
int location = 0;
|
|
||||||
|
|
||||||
/* Uniform blocks */
|
/* Uniform blocks */
|
||||||
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
||||||
if (res.bind_type == ShaderCreateInfo::Resource::BindType::UNIFORM_BUFFER) {
|
if (res.bind_type == ShaderCreateInfo::Resource::BindType::UNIFORM_BUFFER) {
|
||||||
copy_input_name(input, res.image.name, name_buffer_, name_buffer_offset);
|
copy_input_name(input, res.image.name, name_buffer_, name_buffer_offset);
|
||||||
input->location = location++;
|
input->location = input->binding = res.slot;
|
||||||
input->binding = res.slot;
|
|
||||||
input++;
|
input++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,14 +64,12 @@ void VKShaderInterface::init(const shader::ShaderCreateInfo &info)
|
||||||
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
||||||
if (res.bind_type == ShaderCreateInfo::Resource::BindType::SAMPLER) {
|
if (res.bind_type == ShaderCreateInfo::Resource::BindType::SAMPLER) {
|
||||||
copy_input_name(input, res.sampler.name, name_buffer_, name_buffer_offset);
|
copy_input_name(input, res.sampler.name, name_buffer_, name_buffer_offset);
|
||||||
input->location = location++;
|
input->location = input->binding = res.slot;
|
||||||
input->binding = res.slot;
|
|
||||||
input++;
|
input++;
|
||||||
}
|
}
|
||||||
else if (res.bind_type == ShaderCreateInfo::Resource::BindType::IMAGE) {
|
else if (res.bind_type == ShaderCreateInfo::Resource::BindType::IMAGE) {
|
||||||
copy_input_name(input, res.image.name, name_buffer_, name_buffer_offset);
|
copy_input_name(input, res.image.name, name_buffer_, name_buffer_offset);
|
||||||
input->location = location++;
|
input->location = input->binding = res.slot + image_offset_;
|
||||||
input->binding = res.slot + image_offset_;
|
|
||||||
input++;
|
input++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,13 +78,57 @@ void VKShaderInterface::init(const shader::ShaderCreateInfo &info)
|
||||||
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
for (const ShaderCreateInfo::Resource &res : all_resources) {
|
||||||
if (res.bind_type == ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER) {
|
if (res.bind_type == ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER) {
|
||||||
copy_input_name(input, res.storagebuf.name, name_buffer_, name_buffer_offset);
|
copy_input_name(input, res.storagebuf.name, name_buffer_, name_buffer_offset);
|
||||||
input->location = location++;
|
input->location = input->binding = res.slot;
|
||||||
input->binding = res.slot;
|
|
||||||
input++;
|
input++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort_inputs();
|
sort_inputs();
|
||||||
|
|
||||||
|
/* Determine the descriptor set locations after the inputs have been sorted.*/
|
||||||
|
descriptor_set_locations_ = Array<VKDescriptorSet::Location>(input_tot_len);
|
||||||
|
uint32_t descriptor_set_location = 0;
|
||||||
|
for (ShaderCreateInfo::Resource &res : all_resources) {
|
||||||
|
const ShaderInput *input = shader_input_get(res);
|
||||||
|
descriptor_set_location_update(input, descriptor_set_location++);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int32_t shader_input_index(const ShaderInput *shader_inputs,
|
||||||
|
const ShaderInput *shader_input)
|
||||||
|
{
|
||||||
|
int32_t index = (shader_input - shader_inputs);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VKShaderInterface::descriptor_set_location_update(const ShaderInput *shader_input,
|
||||||
|
const VKDescriptorSet::Location location)
|
||||||
|
{
|
||||||
|
int32_t index = shader_input_index(inputs_, shader_input);
|
||||||
|
descriptor_set_locations_[index] = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VKDescriptorSet::Location VKShaderInterface::descriptor_set_location(
|
||||||
|
const ShaderInput *shader_input) const
|
||||||
|
{
|
||||||
|
int32_t index = shader_input_index(inputs_, shader_input);
|
||||||
|
return descriptor_set_locations_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const VKDescriptorSet::Location VKShaderInterface::descriptor_set_location(
|
||||||
|
const shader::ShaderCreateInfo::Resource &resource) const
|
||||||
|
{
|
||||||
|
const ShaderInput *shader_input = shader_input_get(resource);
|
||||||
|
BLI_assert(shader_input);
|
||||||
|
return descriptor_set_location(shader_input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VKDescriptorSet::Location VKShaderInterface::descriptor_set_location(
|
||||||
|
const shader::ShaderCreateInfo::Resource::BindType &bind_type, int binding) const
|
||||||
|
{
|
||||||
|
const ShaderInput *shader_input = shader_input_get(bind_type, binding);
|
||||||
|
BLI_assert(shader_input);
|
||||||
|
return descriptor_set_location(shader_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShaderInput *VKShaderInterface::shader_input_get(
|
const ShaderInput *VKShaderInterface::shader_input_get(
|
||||||
|
|
|
@ -7,9 +7,13 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "BLI_array.hh"
|
||||||
|
|
||||||
#include "gpu_shader_create_info.hh"
|
#include "gpu_shader_create_info.hh"
|
||||||
#include "gpu_shader_interface.hh"
|
#include "gpu_shader_interface.hh"
|
||||||
|
|
||||||
|
#include "vk_descriptor_set.hh"
|
||||||
|
|
||||||
namespace blender::gpu {
|
namespace blender::gpu {
|
||||||
class VKShaderInterface : public ShaderInterface {
|
class VKShaderInterface : public ShaderInterface {
|
||||||
private:
|
private:
|
||||||
|
@ -21,11 +25,19 @@ class VKShaderInterface : public ShaderInterface {
|
||||||
* overlapping.
|
* overlapping.
|
||||||
*/
|
*/
|
||||||
uint32_t image_offset_ = 0;
|
uint32_t image_offset_ = 0;
|
||||||
|
Array<VKDescriptorSet::Location> descriptor_set_locations_;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
VKShaderInterface() = default;
|
VKShaderInterface() = default;
|
||||||
|
|
||||||
void init(const shader::ShaderCreateInfo &info);
|
void init(const shader::ShaderCreateInfo &info);
|
||||||
|
|
||||||
|
const VKDescriptorSet::Location descriptor_set_location(
|
||||||
|
const shader::ShaderCreateInfo::Resource &resource) const;
|
||||||
|
const VKDescriptorSet::Location descriptor_set_location(
|
||||||
|
const shader::ShaderCreateInfo::Resource::BindType &bind_type, int binding) const;
|
||||||
|
|
||||||
|
private:
|
||||||
/**
|
/**
|
||||||
* Retrieve the shader input for the given resource.
|
* Retrieve the shader input for the given resource.
|
||||||
*
|
*
|
||||||
|
@ -35,5 +47,9 @@ class VKShaderInterface : public ShaderInterface {
|
||||||
const ShaderInput *shader_input_get(const shader::ShaderCreateInfo::Resource &resource) const;
|
const ShaderInput *shader_input_get(const shader::ShaderCreateInfo::Resource &resource) const;
|
||||||
const ShaderInput *shader_input_get(
|
const ShaderInput *shader_input_get(
|
||||||
const shader::ShaderCreateInfo::Resource::BindType &bind_type, int binding) const;
|
const shader::ShaderCreateInfo::Resource::BindType &bind_type, int binding) const;
|
||||||
|
const VKDescriptorSet::Location descriptor_set_location(const ShaderInput *shader_input) const;
|
||||||
|
void descriptor_set_location_update(const ShaderInput *shader_input,
|
||||||
|
const VKDescriptorSet::Location location);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace blender::gpu
|
} // namespace blender::gpu
|
||||||
|
|
|
@ -34,9 +34,9 @@ void VKStorageBuffer::bind(int slot)
|
||||||
}
|
}
|
||||||
VKShader *shader = static_cast<VKShader *>(context.shader);
|
VKShader *shader = static_cast<VKShader *>(context.shader);
|
||||||
const VKShaderInterface &shader_interface = shader->interface_get();
|
const VKShaderInterface &shader_interface = shader->interface_get();
|
||||||
const ShaderInput *shader_input = shader_interface.shader_input_get(
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(
|
||||||
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, slot);
|
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, slot);
|
||||||
shader->pipeline_get().descriptor_set_get().bind(*this, shader_input);
|
shader->pipeline_get().descriptor_set_get().bind(*this, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VKStorageBuffer::unbind()
|
void VKStorageBuffer::unbind()
|
||||||
|
|
|
@ -227,8 +227,9 @@ void VKTexture::image_bind(int binding)
|
||||||
}
|
}
|
||||||
VKContext &context = *VKContext::get();
|
VKContext &context = *VKContext::get();
|
||||||
VKShader *shader = static_cast<VKShader *>(context.shader);
|
VKShader *shader = static_cast<VKShader *>(context.shader);
|
||||||
VKDescriptorSet::Location location(shader->interface_get().shader_input_get(
|
const VKShaderInterface &shader_interface = shader->interface_get();
|
||||||
shader::ShaderCreateInfo::Resource::BindType::IMAGE, binding));
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(
|
||||||
|
shader::ShaderCreateInfo::Resource::BindType::IMAGE, binding);
|
||||||
shader->pipeline_get().descriptor_set_get().image_bind(*this, location);
|
shader->pipeline_get().descriptor_set_get().image_bind(*this, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,9 @@ void VKVertexBuffer::bind_as_ssbo(uint binding)
|
||||||
|
|
||||||
VKShader *shader = static_cast<VKShader *>(context.shader);
|
VKShader *shader = static_cast<VKShader *>(context.shader);
|
||||||
const VKShaderInterface &shader_interface = shader->interface_get();
|
const VKShaderInterface &shader_interface = shader->interface_get();
|
||||||
const ShaderInput *shader_input = shader_interface.shader_input_get(
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(
|
||||||
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, binding);
|
shader::ShaderCreateInfo::Resource::BindType::STORAGE_BUFFER, binding);
|
||||||
shader->pipeline_get().descriptor_set_get().bind_as_ssbo(*this, shader_input);
|
shader->pipeline_get().descriptor_set_get().bind_as_ssbo(*this, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VKVertexBuffer::bind_as_texture(uint /*binding*/)
|
void VKVertexBuffer::bind_as_texture(uint /*binding*/)
|
||||||
|
|
Loading…
Reference in New Issue