video_core: fix texture filters in GLES
anime4k: reset state.texture_units[0].texture_2d before filtering clang-fmt
This commit is contained in:
parent
7b82883767
commit
91f52c2fdb
11 changed files with 88 additions and 102 deletions
|
@ -100,10 +100,8 @@ add_library(video_core STATIC
|
|||
|
||||
set(SHADER_FILES
|
||||
renderer_opengl/texture_filters/anime4k/refine.frag
|
||||
renderer_opengl/texture_filters/anime4k/refine.vert
|
||||
renderer_opengl/texture_filters/anime4k/x_gradient.frag
|
||||
renderer_opengl/texture_filters/anime4k/y_gradient.frag
|
||||
renderer_opengl/texture_filters/anime4k/y_gradient.vert
|
||||
renderer_opengl/texture_filters/bicubic/bicubic.frag
|
||||
renderer_opengl/texture_filters/scale_force/scale_force.frag
|
||||
renderer_opengl/texture_filters/tex_coord.vert
|
||||
|
|
|
@ -34,30 +34,14 @@
|
|||
#include "video_core/renderer_opengl/texture_filters/anime4k/anime4k_ultrafast.h"
|
||||
|
||||
#include "shaders/refine.frag"
|
||||
#include "shaders/refine.vert"
|
||||
#include "shaders/tex_coord.vert"
|
||||
#include "shaders/x_gradient.frag"
|
||||
#include "shaders/y_gradient.frag"
|
||||
#include "shaders/y_gradient.vert"
|
||||
|
||||
namespace OpenGL {
|
||||
|
||||
Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_factor) {
|
||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||
const auto setup_temp_tex = [this](TempTex& texture, GLint internal_format, GLint format) {
|
||||
texture.fbo.Create();
|
||||
texture.tex.Create();
|
||||
state.draw.draw_framebuffer = texture.fbo.handle;
|
||||
state.Apply();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE, texture.tex.handle);
|
||||
glTexImage2D(GL_TEXTURE_RECTANGLE, 0, internal_format, 1024 * internal_scale_factor,
|
||||
1024 * internal_scale_factor, 0, format, GL_HALF_FLOAT, nullptr);
|
||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE,
|
||||
texture.tex.handle, 0);
|
||||
};
|
||||
setup_temp_tex(LUMAD, GL_R16F, GL_RED);
|
||||
setup_temp_tex(XY, GL_RG16F, GL_RG);
|
||||
|
||||
vao.Create();
|
||||
|
||||
|
@ -65,17 +49,17 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_f
|
|||
samplers[idx].Create();
|
||||
state.texture_units[idx].sampler = samplers[idx].handle;
|
||||
glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_MIN_FILTER,
|
||||
idx == 0 ? GL_LINEAR : GL_NEAREST);
|
||||
idx != 2 ? GL_LINEAR : GL_NEAREST);
|
||||
glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_MAG_FILTER,
|
||||
idx == 0 ? GL_LINEAR : GL_NEAREST);
|
||||
idx != 2 ? GL_LINEAR : GL_NEAREST);
|
||||
glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glSamplerParameteri(samplers[idx].handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
state.draw.vertex_array = vao.handle;
|
||||
|
||||
gradient_x_program.Create(tex_coord_vert.data(), x_gradient_frag.data());
|
||||
gradient_y_program.Create(y_gradient_vert.data(), y_gradient_frag.data());
|
||||
refine_program.Create(refine_vert.data(), refine_frag.data());
|
||||
gradient_y_program.Create(tex_coord_vert.data(), y_gradient_frag.data());
|
||||
refine_program.Create(tex_coord_vert.data(), refine_frag.data());
|
||||
|
||||
state.draw.shader_program = gradient_y_program.handle;
|
||||
state.Apply();
|
||||
|
@ -84,8 +68,6 @@ Anime4kUltrafast::Anime4kUltrafast(u16 scale_factor) : TextureFilterBase(scale_f
|
|||
state.draw.shader_program = refine_program.handle;
|
||||
state.Apply();
|
||||
glUniform1i(glGetUniformLocation(refine_program.handle, "LUMAD"), 1);
|
||||
glUniform1f(glGetUniformLocation(refine_program.handle, "final_scale"),
|
||||
static_cast<GLfloat>(internal_scale_factor) / scale_factor);
|
||||
|
||||
cur_state.Apply();
|
||||
}
|
||||
|
@ -95,20 +77,48 @@ void Anime4kUltrafast::Filter(GLuint src_tex, const Common::Rectangle<u32>& src_
|
|||
GLuint read_fb_handle, GLuint draw_fb_handle) {
|
||||
const OpenGLState cur_state = OpenGLState::GetCurState();
|
||||
|
||||
// These will have handles from the previous texture that was filtered, reset them to avoid
|
||||
// binding invalid textures.
|
||||
state.texture_units[0].texture_2d = 0;
|
||||
state.texture_units[1].texture_2d = 0;
|
||||
state.texture_units[2].texture_2d = 0;
|
||||
|
||||
const auto setup_temp_tex = [this, &src_rect](GLint internal_format, GLint format) {
|
||||
TempTex texture;
|
||||
texture.fbo.Create();
|
||||
texture.tex.Create();
|
||||
state.texture_units[0].texture_2d = texture.tex.handle;
|
||||
state.draw.draw_framebuffer = texture.fbo.handle;
|
||||
state.Apply();
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, texture.tex.handle);
|
||||
if (GL_ARB_texture_storage) {
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, internal_format,
|
||||
src_rect.GetWidth() * internal_scale_factor,
|
||||
src_rect.GetHeight() * internal_scale_factor);
|
||||
} else {
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, internal_format, src_rect.GetWidth() * internal_scale_factor,
|
||||
src_rect.GetHeight() * internal_scale_factor, 0, format, GL_HALF_FLOAT, nullptr);
|
||||
}
|
||||
glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
|
||||
texture.tex.handle, 0);
|
||||
return texture;
|
||||
};
|
||||
auto XY = setup_temp_tex(GL_RG16F, GL_RG);
|
||||
auto LUMAD = setup_temp_tex(GL_R16F, GL_RED);
|
||||
|
||||
state.viewport = {static_cast<GLint>(src_rect.left * internal_scale_factor),
|
||||
static_cast<GLint>(src_rect.bottom * internal_scale_factor),
|
||||
static_cast<GLsizei>(src_rect.GetWidth() * internal_scale_factor),
|
||||
static_cast<GLsizei>(src_rect.GetHeight() * internal_scale_factor)};
|
||||
state.texture_units[0].texture_2d = src_tex;
|
||||
state.texture_units[1].texture_2d = LUMAD.tex.handle;
|
||||
state.texture_units[2].texture_2d = XY.tex.handle;
|
||||
state.draw.draw_framebuffer = XY.fbo.handle;
|
||||
state.draw.shader_program = gradient_x_program.handle;
|
||||
state.Apply();
|
||||
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE, LUMAD.tex.handle);
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
glBindTexture(GL_TEXTURE_RECTANGLE, XY.tex.handle);
|
||||
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
// gradient y pass
|
||||
|
|
|
@ -30,8 +30,6 @@ private:
|
|||
OGLTexture tex;
|
||||
OGLFramebuffer fbo;
|
||||
};
|
||||
TempTex LUMAD;
|
||||
TempTex XY;
|
||||
|
||||
std::array<OGLSampler, 3> samplers;
|
||||
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
//? #version 330
|
||||
precision mediump float;
|
||||
|
||||
in vec2 tex_coord;
|
||||
in vec2 input_max;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
||||
uniform sampler2D HOOKED;
|
||||
uniform sampler2DRect LUMAD;
|
||||
uniform sampler2DRect LUMAG;
|
||||
|
||||
uniform float final_scale;
|
||||
uniform sampler2D LUMAD;
|
||||
|
||||
const float LINE_DETECT_THRESHOLD = 0.4;
|
||||
const float STRENGTH = 0.6;
|
||||
|
@ -21,12 +19,12 @@ struct RGBAL {
|
|||
};
|
||||
|
||||
vec4 getAverage(vec4 cc, vec4 a, vec4 b, vec4 c) {
|
||||
return cc * (1 - STRENGTH) + ((a + b + c) / 3) * STRENGTH;
|
||||
return cc * (1.0 - STRENGTH) + ((a + b + c) / 3.0) * STRENGTH;
|
||||
}
|
||||
|
||||
#define GetRGBAL(offset) \
|
||||
RGBAL(textureOffset(HOOKED, tex_coord, offset), \
|
||||
texture(LUMAD, clamp((gl_FragCoord.xy + offset) * final_scale, vec2(0.0), input_max)).x)
|
||||
#define GetRGBAL(x_offset, y_offset) \
|
||||
RGBAL(textureLodOffset(HOOKED, tex_coord, 0.0, ivec2(x_offset, y_offset)), \
|
||||
textureLodOffset(LUMAD, tex_coord, 0.0, ivec2(x_offset, y_offset)).x)
|
||||
|
||||
float min3v(float a, float b, float c) {
|
||||
return min(min(a, b), c);
|
||||
|
@ -37,23 +35,23 @@ float max3v(float a, float b, float c) {
|
|||
}
|
||||
|
||||
vec4 Compute() {
|
||||
RGBAL cc = GetRGBAL(ivec2(0));
|
||||
RGBAL cc = GetRGBAL(0, 0);
|
||||
|
||||
if (cc.l > LINE_DETECT_THRESHOLD) {
|
||||
return cc.c;
|
||||
}
|
||||
|
||||
RGBAL tl = GetRGBAL(ivec2(-1, -1));
|
||||
RGBAL t = GetRGBAL(ivec2(0, -1));
|
||||
RGBAL tr = GetRGBAL(ivec2(1, -1));
|
||||
RGBAL tl = GetRGBAL(-1, -1);
|
||||
RGBAL t = GetRGBAL(0, -1);
|
||||
RGBAL tr = GetRGBAL(1, -1);
|
||||
|
||||
RGBAL l = GetRGBAL(ivec2(-1, 0));
|
||||
RGBAL l = GetRGBAL(-1, 0);
|
||||
|
||||
RGBAL r = GetRGBAL(ivec2(1, 0));
|
||||
RGBAL r = GetRGBAL(1, 0);
|
||||
|
||||
RGBAL bl = GetRGBAL(ivec2(-1, 1));
|
||||
RGBAL b = GetRGBAL(ivec2(0, 1));
|
||||
RGBAL br = GetRGBAL(ivec2(1, 1));
|
||||
RGBAL bl = GetRGBAL(-1, 1);
|
||||
RGBAL b = GetRGBAL(0, 1);
|
||||
RGBAL br = GetRGBAL(1, 1);
|
||||
|
||||
// Kernel 0 and 4
|
||||
float maxDark = max3v(br.l, b.l, bl.l);
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
//? #version 330
|
||||
out vec2 tex_coord;
|
||||
out vec2 input_max;
|
||||
|
||||
uniform sampler2D HOOKED;
|
||||
|
||||
const vec2 vertices[4] =
|
||||
vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||
tex_coord = (vertices[gl_VertexID] + 1.0) / 2.0;
|
||||
input_max = textureSize(HOOKED, 0) * 2.0 - 1.0;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
//? #version 330
|
||||
precision mediump float;
|
||||
|
||||
in vec2 tex_coord;
|
||||
|
||||
out vec2 frag_color;
|
||||
|
@ -7,7 +9,7 @@ uniform sampler2D tex_input;
|
|||
|
||||
const vec3 K = vec3(0.2627, 0.6780, 0.0593);
|
||||
// TODO: improve handling of alpha channel
|
||||
#define GetLum(xoffset) dot(K, textureOffset(tex_input, tex_coord, ivec2(xoffset, 0)).rgb)
|
||||
#define GetLum(xoffset) dot(K, textureLodOffset(tex_input, tex_coord, 0.0, ivec2(xoffset, 0)).rgb)
|
||||
|
||||
void main() {
|
||||
float l = GetLum(-1);
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
//? #version 330
|
||||
in vec2 input_max;
|
||||
precision mediump float;
|
||||
|
||||
in vec2 tex_coord;
|
||||
|
||||
out float frag_color;
|
||||
|
||||
uniform sampler2DRect tex_input;
|
||||
uniform sampler2D tex_input;
|
||||
|
||||
void main() {
|
||||
vec2 t = texture(tex_input, min(gl_FragCoord.xy + vec2(0.0, 1.0), input_max)).xy;
|
||||
vec2 c = texture(tex_input, gl_FragCoord.xy).xy;
|
||||
vec2 b = texture(tex_input, max(gl_FragCoord.xy - vec2(0.0, 1.0), vec2(0.0))).xy;
|
||||
vec2 t = textureLodOffset(tex_input, tex_coord, 0.0, ivec2(0, 1)).xy;
|
||||
vec2 c = textureLod(tex_input, tex_coord, 0.0).xy;
|
||||
vec2 b = textureLodOffset(tex_input, tex_coord, 0.0, ivec2(0, -1)).xy;
|
||||
|
||||
vec2 grad = vec2(t.x + 2 * c.x + b.x, b.y - t.y);
|
||||
vec2 grad = vec2(t.x + 2.0 * c.x + b.x, b.y - t.y);
|
||||
|
||||
frag_color = 1 - length(grad);
|
||||
frag_color = 1.0 - length(grad);
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
//? #version 330
|
||||
out vec2 input_max;
|
||||
|
||||
uniform sampler2D tex_size;
|
||||
|
||||
const vec2 vertices[4] =
|
||||
vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||
input_max = textureSize(tex_size, 0) * 2 - 1;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
//? #version 330
|
||||
precision mediump float;
|
||||
|
||||
in vec2 tex_coord;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
@ -18,7 +20,7 @@ vec4 cubic(float v) {
|
|||
|
||||
vec4 textureBicubic(sampler2D sampler, vec2 texCoords) {
|
||||
|
||||
vec2 texSize = textureSize(sampler, 0);
|
||||
vec2 texSize = vec2(textureSize(sampler, 0));
|
||||
vec2 invTexSize = 1.0 / texSize;
|
||||
|
||||
texCoords = texCoords * texSize - 0.5;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//? #version 330
|
||||
precision mediump float;
|
||||
|
||||
in vec2 tex_coord;
|
||||
in vec2 source_size;
|
||||
in vec2 output_size;
|
||||
|
@ -6,7 +8,7 @@ in vec2 output_size;
|
|||
out vec4 frag_color;
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform float scale;
|
||||
uniform lowp float scale;
|
||||
|
||||
const int BLEND_NONE = 0;
|
||||
const int BLEND_NORMAL = 1;
|
||||
|
@ -42,12 +44,12 @@ float GetLeftRatio(vec2 center, vec2 origin, vec2 direction) {
|
|||
return smoothstep(-sqrt(2.0) / 2.0, sqrt(2.0) / 2.0, v);
|
||||
}
|
||||
|
||||
vec2 pos = fract(tex_coord * source_size) - vec2(0.5, 0.5);
|
||||
vec2 coord = tex_coord - pos / source_size;
|
||||
|
||||
#define P(x, y) textureOffset(tex, coord, ivec2(x, y))
|
||||
|
||||
void main() {
|
||||
vec2 pos = fract(tex_coord * source_size) - vec2(0.5, 0.5);
|
||||
vec2 coord = tex_coord - pos / source_size;
|
||||
|
||||
//---------------------------------------
|
||||
// Input Pixel Mapping: -|x|x|x|-
|
||||
// x|A|B|C|x
|
||||
|
@ -142,15 +144,15 @@ void main() {
|
|||
(IsPixEqual(G, H) && IsPixEqual(H, I) && IsPixEqual(I, F) &&
|
||||
IsPixEqual(F, C) && !IsPixEqual(E, I))));
|
||||
vec2 origin = vec2(0.0, 1.0 / sqrt(2.0));
|
||||
ivec2 direction = ivec2(1, -1);
|
||||
vec2 direction = vec2(1.0, -1.0);
|
||||
if (doLineBlend) {
|
||||
bool haveShallowLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_F_G <= dist_H_C) && E != G && D != G;
|
||||
bool haveSteepLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_H_C <= dist_F_G) && E != C && B != C;
|
||||
origin = haveShallowLine ? vec2(0.0, 0.25) : vec2(0.0, 0.5);
|
||||
direction.x += haveShallowLine ? 1 : 0;
|
||||
direction.y -= haveSteepLine ? 1 : 0;
|
||||
direction.x += haveShallowLine ? 1.0 : 0.0;
|
||||
direction.y -= haveSteepLine ? 1.0 : 0.0;
|
||||
}
|
||||
vec4 blendPix = mix(H, F, step(ColorDist(E, F), ColorDist(E, H)));
|
||||
res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
|
||||
|
@ -169,15 +171,15 @@ void main() {
|
|||
(IsPixEqual(A, D) && IsPixEqual(D, G) && IsPixEqual(G, H) &&
|
||||
IsPixEqual(H, I) && !IsPixEqual(E, G))));
|
||||
vec2 origin = vec2(-1.0 / sqrt(2.0), 0.0);
|
||||
ivec2 direction = ivec2(1, 1);
|
||||
vec2 direction = vec2(1.0, 1.0);
|
||||
if (doLineBlend) {
|
||||
bool haveShallowLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_H_A <= dist_D_I) && E != A && B != A;
|
||||
bool haveSteepLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_D_I <= dist_H_A) && E != I && F != I;
|
||||
origin = haveShallowLine ? vec2(-0.25, 0.0) : vec2(-0.5, 0.0);
|
||||
direction.y += haveShallowLine ? 1 : 0;
|
||||
direction.x += haveSteepLine ? 1 : 0;
|
||||
direction.y += haveShallowLine ? 1.0 : 0.0;
|
||||
direction.x += haveSteepLine ? 1.0 : 0.0;
|
||||
}
|
||||
origin = origin;
|
||||
direction = direction;
|
||||
|
@ -198,15 +200,15 @@ void main() {
|
|||
(IsPixEqual(I, F) && IsPixEqual(F, C) && IsPixEqual(C, B) &&
|
||||
IsPixEqual(B, A) && !IsPixEqual(E, C))));
|
||||
vec2 origin = vec2(1.0 / sqrt(2.0), 0.0);
|
||||
ivec2 direction = ivec2(-1, -1);
|
||||
vec2 direction = vec2(-1.0, -1.0);
|
||||
if (doLineBlend) {
|
||||
bool haveShallowLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_B_I <= dist_F_A) && E != I && H != I;
|
||||
bool haveSteepLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_F_A <= dist_B_I) && E != A && D != A;
|
||||
origin = haveShallowLine ? vec2(0.25, 0.0) : vec2(0.5, 0.0);
|
||||
direction.y -= haveShallowLine ? 1 : 0;
|
||||
direction.x -= haveSteepLine ? 1 : 0;
|
||||
direction.y -= haveShallowLine ? 1.0 : 0.0;
|
||||
direction.x -= haveSteepLine ? 1.0 : 0.0;
|
||||
}
|
||||
vec4 blendPix = mix(F, B, step(ColorDist(E, B), ColorDist(E, F)));
|
||||
res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
|
||||
|
@ -225,15 +227,15 @@ void main() {
|
|||
(IsPixEqual(C, B) && IsPixEqual(B, A) && IsPixEqual(A, D) &&
|
||||
IsPixEqual(D, G) && !IsPixEqual(E, A))));
|
||||
vec2 origin = vec2(0.0, -1.0 / sqrt(2.0));
|
||||
ivec2 direction = ivec2(-1, 1);
|
||||
vec2 direction = vec2(-1.0, 1.0);
|
||||
if (doLineBlend) {
|
||||
bool haveShallowLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_D_C <= dist_B_G) && E != C && F != C;
|
||||
bool haveSteepLine =
|
||||
(STEEP_DIRECTION_THRESHOLD * dist_B_G <= dist_D_C) && E != G && H != G;
|
||||
origin = haveShallowLine ? vec2(0.0, -0.25) : vec2(0.0, -0.5);
|
||||
direction.x -= haveShallowLine ? 1 : 0;
|
||||
direction.y += haveSteepLine ? 1 : 0;
|
||||
direction.x -= haveShallowLine ? 1.0 : 0.0;
|
||||
direction.y += haveSteepLine ? 1.0 : 0.0;
|
||||
}
|
||||
vec4 blendPix = mix(D, B, step(ColorDist(E, B), ColorDist(E, D)));
|
||||
res = mix(res, blendPix, GetLeftRatio(pos, origin, direction));
|
||||
|
|
|
@ -4,7 +4,7 @@ out vec2 source_size;
|
|||
out vec2 output_size;
|
||||
|
||||
uniform sampler2D tex;
|
||||
uniform float scale;
|
||||
uniform lowp float scale;
|
||||
|
||||
const vec2 vertices[4] =
|
||||
vec2[4](vec2(-1.0, -1.0), vec2(1.0, -1.0), vec2(-1.0, 1.0), vec2(1.0, 1.0));
|
||||
|
@ -12,6 +12,6 @@ const vec2 vertices[4] =
|
|||
void main() {
|
||||
gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0);
|
||||
tex_coord = (vertices[gl_VertexID] + 1.0) / 2.0;
|
||||
source_size = textureSize(tex, 0);
|
||||
source_size = vec2(textureSize(tex, 0));
|
||||
output_size = source_size * scale;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue