mirror of
https://github.com/w23/xash3d-fwgs
synced 2024-12-14 21:20:26 +01:00
vk: improve traditional blending universally
it is now almost on par with the gl renderer
This commit is contained in:
parent
ce27bdb1b1
commit
9f72a804e0
@ -390,8 +390,8 @@ static void R_DrawSegs( vec3_t source, vec3_t delta, float width, float scale, f
|
||||
.emissive = { color[0], color[1], color[2] },
|
||||
};
|
||||
|
||||
|
||||
VK_RenderModelDynamicBegin( render_mode, color, "beam" /* TODO its name */ );
|
||||
vk_render_type_e render_type = render_mode == kRenderNormal ? kVkRenderTypeSolid : kVkRenderType_A_1_R;
|
||||
VK_RenderModelDynamicBegin( render_type, color, "beam" /* TODO its name */ );
|
||||
VK_RenderModelDynamicAddGeometry( &geometry );
|
||||
VK_RenderModelDynamicCommit();
|
||||
}
|
||||
|
@ -234,6 +234,20 @@ static void EmitWaterPolys( const cl_entity_t *ent, const msurface_t *warp, qboo
|
||||
// FIXME VK GL_SetupFogColorForSurfaces();
|
||||
}
|
||||
|
||||
static vk_render_type_e brushRenderModeToRenderType( int render_mode ) {
|
||||
switch (render_mode) {
|
||||
case kRenderNormal: return kVkRenderTypeSolid;
|
||||
case kRenderTransColor: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderTransTexture: return kVkRenderType_A_1mA_R;
|
||||
case kRenderGlow: return kVkRenderType_A_1mA_R;
|
||||
case kRenderTransAlpha: return kVkRenderType_AT;
|
||||
case kRenderTransAdd: return kVkRenderType_A_1_R;
|
||||
default: ASSERT(!"Unxpected render_mode");
|
||||
}
|
||||
|
||||
return kVkRenderTypeSolid;
|
||||
}
|
||||
|
||||
static void brushDrawWaterSurfaces( const cl_entity_t *ent, const vec4_t color )
|
||||
{
|
||||
const model_t *model = ent->model;
|
||||
@ -258,7 +272,7 @@ static void brushDrawWaterSurfaces( const cl_entity_t *ent, const vec4_t color )
|
||||
// if( R_CullBox( mins, maxs ))
|
||||
// return;
|
||||
|
||||
VK_RenderModelDynamicBegin( ent->curstate.rendermode, color, "%s water", model->name );
|
||||
VK_RenderModelDynamicBegin( brushRenderModeToRenderType(ent->curstate.rendermode), color, "%s water", model->name );
|
||||
|
||||
// Iterate through all surfaces, find *TURB*
|
||||
for( int i = 0; i < model->nummodelsurfaces; i++ )
|
||||
@ -389,18 +403,7 @@ void VK_BrushModelDraw( const cl_entity_t *ent, int render_mode, float blend, co
|
||||
}
|
||||
}
|
||||
|
||||
if (render_mode == kRenderTransColor) {
|
||||
Vector4Set(bmodel->render_model.color,
|
||||
ent->curstate.rendercolor.r / 255.f,
|
||||
ent->curstate.rendercolor.g / 255.f,
|
||||
ent->curstate.rendercolor.b / 255.f,
|
||||
blend);
|
||||
} else {
|
||||
// Other render modes are not affected by entity current state color
|
||||
Vector4Set(bmodel->render_model.color, 1, 1, 1, blend);
|
||||
}
|
||||
|
||||
bmodel->render_model.render_mode = render_mode;
|
||||
bmodel->render_model.render_type = brushRenderModeToRenderType(render_mode);
|
||||
VK_RenderModelDraw(ent, &bmodel->render_model);
|
||||
}
|
||||
|
||||
@ -700,7 +703,7 @@ qboolean VK_BrushModelLoad( model_t *mod, qboolean map )
|
||||
vk_brush_model_t *bmodel = Mem_Calloc(vk_core.pool, model_size);
|
||||
mod->cache.data = bmodel;
|
||||
Q_strncpy(bmodel->render_model.debug_name, mod->name, sizeof(bmodel->render_model.debug_name));
|
||||
bmodel->render_model.render_mode = kRenderNormal;
|
||||
bmodel->render_model.render_type = kVkRenderTypeSolid;
|
||||
bmodel->render_model.static_map = map;
|
||||
|
||||
bmodel->num_water_surfaces = sizes.water_surfaces;
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include "crtlib.h"
|
||||
|
||||
#define ASSERT(x) if(!( x )) gEngine.Host_Error( "assert %s failed at %s:%d\n", #x, __FILE__, __LINE__ )
|
||||
// TODO ASSERTF(x, fmt, ...)
|
||||
|
||||
#define Mem_Malloc( pool, size ) gEngine._Mem_Alloc( pool, size, false, __FILE__, __LINE__ )
|
||||
#define Mem_Calloc( pool, size ) gEngine._Mem_Alloc( pool, size, true, __FILE__, __LINE__ )
|
||||
|
@ -444,31 +444,30 @@ void VK_RayFrameAddModel( vk_ray_model_t *model, const vk_render_model_t *render
|
||||
memcpy(draw_model->transform_row, *transform_row, sizeof(draw_model->transform_row));
|
||||
}
|
||||
|
||||
switch (render_model->render_mode) {
|
||||
case kRenderNormal:
|
||||
switch (render_model->render_type) {
|
||||
case kVkRenderTypeSolid:
|
||||
draw_model->material_mode = MaterialMode_Opaque;
|
||||
break;
|
||||
|
||||
// C = (1 - alpha) * DST + alpha * SRC (TODO is this right?)
|
||||
case kRenderTransColor:
|
||||
case kRenderTransTexture:
|
||||
HACK_reflective = true;
|
||||
draw_model->material_mode = MaterialMode_Refractive;
|
||||
break;
|
||||
|
||||
// Additive blending: C = SRC * alpha + DST
|
||||
case kRenderGlow:
|
||||
case kRenderTransAdd:
|
||||
case kVkRenderType_A_1mA_RW: // blend: scr*a + dst*(1-a), depth: RW
|
||||
case kVkRenderType_A_1mA_R: // blend: scr*a + dst*(1-a), depth test
|
||||
// FIXME proper trasnlucency
|
||||
//HACK_reflective = true;
|
||||
//draw_model->material_mode = MaterialMode_Refractive;
|
||||
draw_model->material_mode = MaterialMode_Additive;
|
||||
break;
|
||||
|
||||
// Alpha test (TODO additive? mixing?)
|
||||
case kRenderTransAlpha:
|
||||
case kVkRenderType_A_1: // blend: scr*a + dst, no depth test or write
|
||||
case kVkRenderType_A_1_R: // blend: scr*a + dst, depth test
|
||||
case kVkRenderType_1_1_R: // blend: scr + dst, depth test
|
||||
draw_model->material_mode = MaterialMode_Additive;
|
||||
break;
|
||||
|
||||
case kVkRenderType_AT: // no blend, depth RW, alpha test
|
||||
draw_model->material_mode = MaterialMode_Opaque_AlphaTest;
|
||||
break;
|
||||
|
||||
default:
|
||||
gEngine.Host_Error("Unexpected render mode %d\n", render_model->render_mode);
|
||||
gEngine.Host_Error("Unexpected render type %d\n", render_model->render_type);
|
||||
}
|
||||
|
||||
// TODO optimize:
|
||||
|
@ -30,7 +30,7 @@ typedef struct {
|
||||
|
||||
static struct {
|
||||
VkPipelineLayout pipeline_layout;
|
||||
VkPipeline pipelines[kRenderTransAdd + 1];
|
||||
VkPipeline pipelines[kVkRenderType_COUNT];
|
||||
|
||||
vk_buffer_t uniform_buffer;
|
||||
uint32_t ubo_align;
|
||||
@ -121,20 +121,20 @@ static qboolean createPipelines( void )
|
||||
.cullMode = VK_CULL_MODE_FRONT_BIT,
|
||||
};
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(g_render.pipelines); ++i)
|
||||
for (int i = 0; i < kVkRenderType_COUNT; ++i)
|
||||
{
|
||||
const char *name = "UNDEFINED";
|
||||
switch (i)
|
||||
{
|
||||
case kRenderNormal:
|
||||
case kVkRenderTypeSolid:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.blendEnable = VK_FALSE;
|
||||
ci.depthWriteEnable = VK_TRUE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
name = "kRenderNormal";
|
||||
name = "kVkRenderTypeSolid";
|
||||
break;
|
||||
|
||||
case kRenderTransColor:
|
||||
case kVkRenderType_A_1mA_RW:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.depthWriteEnable = VK_TRUE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
@ -142,10 +142,10 @@ static qboolean createPipelines( void )
|
||||
ci.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
ci.srcAlphaBlendFactor = ci.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
ci.dstAlphaBlendFactor = ci.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
name = "kRenderTransColor";
|
||||
name = "kVkRenderType_A_1mA_RW";
|
||||
break;
|
||||
|
||||
case kRenderTransTexture:
|
||||
case kVkRenderType_A_1mA_R:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.depthWriteEnable = VK_FALSE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
@ -153,10 +153,10 @@ static qboolean createPipelines( void )
|
||||
ci.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
ci.srcAlphaBlendFactor = ci.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
ci.dstAlphaBlendFactor = ci.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
name = "kRenderTransTexture";
|
||||
name = "kVkRenderType_A_1mA_R";
|
||||
break;
|
||||
|
||||
case kRenderGlow:
|
||||
case kVkRenderType_A_1:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.depthWriteEnable = VK_FALSE;
|
||||
ci.depthTestEnable = VK_FALSE; // Fake bloom, should be over geometry too
|
||||
@ -164,19 +164,10 @@ static qboolean createPipelines( void )
|
||||
ci.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
ci.srcAlphaBlendFactor = ci.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
ci.dstAlphaBlendFactor = ci.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||
name = "kRenderGlow";
|
||||
name = "kVkRenderType_A_1";
|
||||
break;
|
||||
|
||||
case kRenderTransAlpha:
|
||||
// FIXME sprites are different: they don't do alpha test, but do blending
|
||||
spec_data.alpha_test_threshold = .25f;
|
||||
ci.depthWriteEnable = VK_TRUE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
ci.blendEnable = VK_FALSE;
|
||||
name = "kRenderTransAlpha(test)";
|
||||
break;
|
||||
|
||||
case kRenderTransAdd:
|
||||
case kVkRenderType_A_1_R:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.depthWriteEnable = VK_FALSE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
@ -184,7 +175,26 @@ static qboolean createPipelines( void )
|
||||
ci.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
ci.srcAlphaBlendFactor = ci.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
ci.dstAlphaBlendFactor = ci.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||
name = "kRenderTransAdd";
|
||||
name = "kVkRenderType_A_1_R";
|
||||
break;
|
||||
|
||||
case kVkRenderType_AT:
|
||||
spec_data.alpha_test_threshold = .25f;
|
||||
ci.depthWriteEnable = VK_TRUE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
ci.blendEnable = VK_FALSE;
|
||||
name = "kVkRenderType_AT";
|
||||
break;
|
||||
|
||||
case kVkRenderType_1_1_R:
|
||||
spec_data.alpha_test_threshold = 0.f;
|
||||
ci.depthWriteEnable = VK_FALSE;
|
||||
ci.depthTestEnable = VK_TRUE;
|
||||
ci.blendEnable = VK_TRUE;
|
||||
ci.colorBlendOp = VK_BLEND_OP_ADD;
|
||||
ci.srcAlphaBlendFactor = ci.srcColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||
ci.dstAlphaBlendFactor = ci.dstColorBlendFactor = VK_BLEND_FACTOR_ONE;
|
||||
name = "kVkRenderType_1_1_R";
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -228,7 +238,7 @@ typedef struct {
|
||||
|
||||
typedef struct render_draw_s {
|
||||
int lightmap, texture;
|
||||
int render_mode;
|
||||
int pipeline_index;
|
||||
uint32_t element_count;
|
||||
uint32_t index_offset, vertex_offset;
|
||||
/* TODO this should be a separate thing? */ struct { float r, g, b; } emissive;
|
||||
@ -443,8 +453,8 @@ static void drawCmdPushDraw( const render_draw_t *draw )
|
||||
{
|
||||
draw_command_t *draw_command;
|
||||
|
||||
ASSERT(draw->render_mode >= 0);
|
||||
ASSERT(draw->render_mode < ARRAYSIZE(g_render.pipelines));
|
||||
ASSERT(draw->pipeline_index >= 0);
|
||||
ASSERT(draw->pipeline_index < ARRAYSIZE(g_render.pipelines));
|
||||
ASSERT(draw->lightmap >= 0);
|
||||
ASSERT(draw->texture >= 0);
|
||||
|
||||
@ -581,8 +591,8 @@ void VK_RenderEnd( VkCommandBuffer cmdbuf )
|
||||
vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, g_render.pipeline_layout, 0, 1, vk_desc.ubo_sets, 1, &ubo_offset);
|
||||
}
|
||||
|
||||
if (pipeline != draw->draw.draw.render_mode) {
|
||||
pipeline = draw->draw.draw.render_mode;
|
||||
if (pipeline != draw->draw.draw.pipeline_index) {
|
||||
pipeline = draw->draw.draw.pipeline_index;
|
||||
vkCmdBindPipeline(cmdbuf, VK_PIPELINE_BIND_POINT_GRAPHICS, g_render.pipelines[pipeline]);
|
||||
}
|
||||
|
||||
@ -710,7 +720,7 @@ void VK_RenderModelDraw( const cl_entity_t *ent, vk_render_model_t* model ) {
|
||||
render_draw_t draw = {
|
||||
.lightmap = lightmap,
|
||||
.texture = current_texture,
|
||||
.render_mode = model->render_mode,
|
||||
.pipeline_index = model->render_type,
|
||||
.element_count = element_count,
|
||||
.vertex_offset = vertex_offset,
|
||||
.index_offset = index_offset,
|
||||
@ -734,7 +744,7 @@ void VK_RenderModelDraw( const cl_entity_t *ent, vk_render_model_t* model ) {
|
||||
const render_draw_t draw = {
|
||||
.lightmap = lightmap,
|
||||
.texture = current_texture,
|
||||
.render_mode = model->render_mode,
|
||||
.pipeline_index = model->render_type,
|
||||
.element_count = element_count,
|
||||
.vertex_offset = vertex_offset,
|
||||
.index_offset = index_offset,
|
||||
@ -753,7 +763,7 @@ static struct {
|
||||
vk_render_geometry_t geometries[MAX_DYNAMIC_GEOMETRY];
|
||||
} g_dynamic_model = {0};
|
||||
|
||||
void VK_RenderModelDynamicBegin( int render_mode, const vec4_t color, const char *debug_name_fmt, ... ) {
|
||||
void VK_RenderModelDynamicBegin( vk_render_type_e render_type, const vec4_t color, const char *debug_name_fmt, ... ) {
|
||||
va_list argptr;
|
||||
va_start( argptr, debug_name_fmt );
|
||||
vsnprintf(g_dynamic_model.model.debug_name, sizeof(g_dynamic_model.model.debug_name), debug_name_fmt, argptr );
|
||||
@ -762,7 +772,7 @@ void VK_RenderModelDynamicBegin( int render_mode, const vec4_t color, const char
|
||||
ASSERT(!g_dynamic_model.model.geometries);
|
||||
g_dynamic_model.model.geometries = g_dynamic_model.geometries;
|
||||
g_dynamic_model.model.num_geometries = 0;
|
||||
g_dynamic_model.model.render_mode = render_mode;
|
||||
g_dynamic_model.model.render_type = render_type;
|
||||
g_dynamic_model.model.lightmap = 0;
|
||||
Vector4Copy(color, g_dynamic_model.model.color);
|
||||
}
|
||||
|
@ -62,12 +62,22 @@ struct vk_ray_model_s;
|
||||
|
||||
#define MAX_MODEL_NAME_LENGTH 64
|
||||
|
||||
typedef enum {
|
||||
kVkRenderTypeSolid, // no blending, depth RW
|
||||
kVkRenderType_A_1mA_RW, // blend: src*a + dst*(1-a), depth: RW
|
||||
kVkRenderType_A_1mA_R, // blend: src*a + dst*(1-a), depth test
|
||||
kVkRenderType_A_1, // blend: src*a + dst, no depth test or write
|
||||
kVkRenderType_A_1_R, // blend: src*a + dst, depth test
|
||||
kVkRenderType_AT, // no blend, depth RW, alpha test
|
||||
kVkRenderType_1_1_R, // blend: src + dst, depth test
|
||||
kVkRenderType_COUNT
|
||||
} vk_render_type_e;
|
||||
struct rt_light_add_polygon_s;
|
||||
typedef struct vk_render_model_s {
|
||||
char debug_name[MAX_MODEL_NAME_LENGTH];
|
||||
|
||||
// FIXME: brushes, sprites, studio models, etc all treat render_mode differently
|
||||
int render_mode;
|
||||
vk_render_type_e render_type;
|
||||
vec4_t color;
|
||||
int lightmap; // <= 0 if no lightmap
|
||||
|
||||
@ -93,7 +103,7 @@ qboolean VK_RenderModelInit( vk_render_model_t* model );
|
||||
void VK_RenderModelDestroy( vk_render_model_t* model );
|
||||
void VK_RenderModelDraw( const cl_entity_t *ent, vk_render_model_t* model );
|
||||
|
||||
void VK_RenderModelDynamicBegin( int render_mode, const vec4_t color, const char *debug_name_fmt, ... );
|
||||
void VK_RenderModelDynamicBegin( vk_render_type_e render_type, const vec4_t color, const char *debug_name_fmt, ... );
|
||||
void VK_RenderModelDynamicAddGeometry( const vk_render_geometry_t *geom );
|
||||
void VK_RenderModelDynamicCommit( void );
|
||||
|
||||
|
@ -643,6 +643,20 @@ static qboolean spriteIsOccluded( const cl_entity_t *e, vec3_t origin, float *ps
|
||||
return false;
|
||||
}
|
||||
|
||||
static vk_render_type_e spriteRenderModeToRenderType( int render_mode ) {
|
||||
switch (render_mode) {
|
||||
case kRenderNormal: return kVkRenderTypeSolid;
|
||||
case kRenderTransColor: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderTransTexture: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderGlow: return kVkRenderType_A_1;
|
||||
case kRenderTransAlpha: return kVkRenderType_A_1mA_R;
|
||||
case kRenderTransAdd: return kVkRenderType_A_1_R;
|
||||
default: ASSERT(!"Unxpected render_mode");
|
||||
}
|
||||
|
||||
return kVkRenderTypeSolid;
|
||||
}
|
||||
|
||||
static void R_DrawSpriteQuad( const char *debug_name, mspriteframe_t *frame, vec3_t org, vec3_t v_right, vec3_t v_up, float scale, int texture, int render_mode, const vec4_t color ) {
|
||||
r_geometry_buffer_lock_t buffer;
|
||||
if (!R_GeometryBufferAllocAndLock( &buffer, 4, 6, LifetimeSingleFrame )) {
|
||||
@ -719,7 +733,7 @@ static void R_DrawSpriteQuad( const char *debug_name, mspriteframe_t *frame, vec
|
||||
.emissive = {color[0], color[1], color[2]},
|
||||
};
|
||||
|
||||
VK_RenderModelDynamicBegin( render_mode, color, "%s", debug_name );
|
||||
VK_RenderModelDynamicBegin( spriteRenderModeToRenderType(render_mode), color, "%s", debug_name );
|
||||
VK_RenderModelDynamicAddGeometry( &geometry );
|
||||
VK_RenderModelDynamicCommit();
|
||||
}
|
||||
|
@ -2066,6 +2066,20 @@ _inline void R_StudioDrawChromeMesh( short *ptricmds, vec3_t *pstudionorms, floa
|
||||
}
|
||||
*/
|
||||
|
||||
static vk_render_type_e studioRenderModeToRenderType( int render_mode ) {
|
||||
switch (render_mode) {
|
||||
case kRenderNormal: return kVkRenderTypeSolid;
|
||||
case kRenderTransColor: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderTransTexture: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderGlow: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderTransAlpha: return kVkRenderType_A_1mA_RW;
|
||||
case kRenderTransAdd: return kVkRenderType_1_1_R;
|
||||
default: ASSERT(!"Unxpected render_mode");
|
||||
}
|
||||
|
||||
return kVkRenderTypeSolid;
|
||||
}
|
||||
|
||||
static void R_StudioDrawPoints( void )
|
||||
{
|
||||
int i, j, k, m_skinnum;
|
||||
@ -2082,24 +2096,11 @@ static void R_StudioDrawPoints( void )
|
||||
|
||||
if( !m_pStudioHeader ) return;
|
||||
|
||||
vec4_t color = {1, 1, 1, 1};
|
||||
switch (g_studio.rendermode2) {
|
||||
case kRenderNormal:
|
||||
break;
|
||||
case kRenderTransColor:
|
||||
// TODO pglBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
|
||||
break;
|
||||
case kRenderTransAdd:
|
||||
// TODO pglBlendFunc( GL_ONE, GL_ONE );
|
||||
// TODO pglDepthMask( GL_FALSE );
|
||||
Vector4Set(color, g_studio.blend, g_studio.blend, g_studio.blend, 1.f);
|
||||
break;
|
||||
default:
|
||||
// TODO pglBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
|
||||
Vector4Set(color, 1.f, 1.f, 1.f, g_studio.blend);
|
||||
break;
|
||||
vec4_t color = {1, 1, 1, g_studio.blend};
|
||||
if (g_studio.rendermode2 == kRenderTransAdd) {
|
||||
Vector4Set(color, g_studio.blend, g_studio.blend, g_studio.blend, 1.f);
|
||||
}
|
||||
VK_RenderModelDynamicBegin( RI.currententity->curstate.rendermode, color, "%s", m_pSubModel->name );
|
||||
VK_RenderModelDynamicBegin( studioRenderModeToRenderType(RI.currententity->curstate.rendermode), color, "%s", m_pSubModel->name );
|
||||
|
||||
g_studio.numverts = g_studio.numelems = 0;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user