1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-20 12:15:07 +00:00
serenity/Userland/Libraries/LibGL/Lighting.cpp
Jelle Raaijmakers a074b7e871 LibGL: Support glLightModel inside lists
We now dereference the pointer given to us before adding the arguments
to an active list. This also factors out the switching logic from the
API wrappers, which helps us with a future commit where we autogenerate
all API wrapper functions.
2022-12-20 10:42:31 +01:00

561 lines
22 KiB
C++

/*
* Copyright (c) 2021, Jesse Buhagiar <jooster669@gmail.com>
* Copyright (c) 2021, Stephan Unverwerth <s.unverwerth@serenityos.org>
* Copyright (c) 2022, Jelle Raaijmakers <jelle@gmta.nl>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <LibGL/GLContext.h>
namespace GL {
template<typename T>
void GLContext::get_light_param(GLenum light, GLenum pname, T* params)
{
auto const& light_state = m_light_states[light - GL_LIGHT0];
switch (pname) {
case GL_AMBIENT:
params[0] = light_state.ambient_intensity.x();
params[1] = light_state.ambient_intensity.y();
params[2] = light_state.ambient_intensity.z();
params[3] = light_state.ambient_intensity.w();
break;
case GL_DIFFUSE:
params[0] = light_state.diffuse_intensity.x();
params[1] = light_state.diffuse_intensity.y();
params[2] = light_state.diffuse_intensity.z();
params[3] = light_state.diffuse_intensity.w();
break;
case GL_SPECULAR:
params[0] = light_state.specular_intensity.x();
params[1] = light_state.specular_intensity.y();
params[2] = light_state.specular_intensity.z();
params[3] = light_state.specular_intensity.w();
break;
case GL_SPOT_DIRECTION:
params[0] = light_state.spotlight_direction.x();
params[1] = light_state.spotlight_direction.y();
params[2] = light_state.spotlight_direction.z();
break;
case GL_SPOT_EXPONENT:
*params = light_state.spotlight_exponent;
break;
case GL_SPOT_CUTOFF:
*params = light_state.spotlight_cutoff_angle;
break;
case GL_CONSTANT_ATTENUATION:
*params = light_state.constant_attenuation;
break;
case GL_LINEAR_ATTENUATION:
*params = light_state.linear_attenuation;
break;
case GL_QUADRATIC_ATTENUATION:
*params = light_state.quadratic_attenuation;
break;
}
}
template<typename T>
void GLContext::get_material_param(Face face, GLenum pname, T* params)
{
auto const& material = m_material_states[face];
switch (pname) {
case GL_AMBIENT:
params[0] = static_cast<T>(material.ambient.x());
params[1] = static_cast<T>(material.ambient.y());
params[2] = static_cast<T>(material.ambient.z());
params[3] = static_cast<T>(material.ambient.w());
break;
case GL_DIFFUSE:
params[0] = static_cast<T>(material.diffuse.x());
params[1] = static_cast<T>(material.diffuse.y());
params[2] = static_cast<T>(material.diffuse.z());
params[3] = static_cast<T>(material.diffuse.w());
break;
case GL_SPECULAR:
params[0] = static_cast<T>(material.specular.x());
params[1] = static_cast<T>(material.specular.y());
params[2] = static_cast<T>(material.specular.z());
params[3] = static_cast<T>(material.specular.w());
break;
case GL_EMISSION:
params[0] = static_cast<T>(material.emissive.x());
params[1] = static_cast<T>(material.emissive.y());
params[2] = static_cast<T>(material.emissive.z());
params[3] = static_cast<T>(material.emissive.w());
break;
case GL_SHININESS:
*params = material.shininess;
break;
}
}
void GLContext::gl_color_material(GLenum face, GLenum mode)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_color_material, face, mode);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(face != GL_FRONT
&& face != GL_BACK
&& face != GL_FRONT_AND_BACK,
GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(mode != GL_EMISSION
&& mode != GL_AMBIENT
&& mode != GL_DIFFUSE
&& mode != GL_SPECULAR
&& mode != GL_AMBIENT_AND_DIFFUSE,
GL_INVALID_ENUM);
m_color_material_face = face;
m_color_material_mode = mode;
m_light_state_is_dirty = true;
}
void GLContext::gl_get_light(GLenum light, GLenum pname, void* params, GLenum type)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_get_light, light, pname, params, type);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(light < GL_LIGHT0 || light > GL_LIGHT0 + m_device_info.num_lights, GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_SPOT_DIRECTION || pname == GL_SPOT_EXPONENT || pname == GL_SPOT_CUTOFF || pname == GL_CONSTANT_ATTENUATION || pname == GL_LINEAR_ATTENUATION || pname == GL_QUADRATIC_ATTENUATION), GL_INVALID_ENUM);
if (type == GL_FLOAT)
get_light_param<GLfloat>(light, pname, static_cast<GLfloat*>(params));
else if (type == GL_INT)
get_light_param<GLint>(light, pname, static_cast<GLint*>(params));
else
VERIFY_NOT_REACHED();
}
void GLContext::gl_get_material(GLenum face, GLenum pname, void* params, GLenum type)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_get_material, face, pname, params, type);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_EMISSION), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(face == GL_FRONT || face == GL_BACK), GL_INVALID_ENUM);
Face material_face = Front;
switch (face) {
case GL_FRONT:
material_face = Front;
break;
case GL_BACK:
material_face = Back;
break;
}
if (type == GL_FLOAT)
get_material_param<GLfloat>(material_face, pname, static_cast<GLfloat*>(params));
else if (type == GL_INT)
get_material_param<GLint>(material_face, pname, static_cast<GLint*>(params));
else
VERIFY_NOT_REACHED();
}
void GLContext::gl_light_model(GLenum pname, GLfloat x, GLfloat y, GLfloat z, GLfloat w)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_light_model, pname, x, y, z, w);
RETURN_WITH_ERROR_IF(pname != GL_LIGHT_MODEL_AMBIENT
&& pname != GL_LIGHT_MODEL_COLOR_CONTROL
&& pname != GL_LIGHT_MODEL_LOCAL_VIEWER
&& pname != GL_LIGHT_MODEL_TWO_SIDE,
GL_INVALID_ENUM);
auto lighting_params = m_rasterizer->light_model();
switch (pname) {
case GL_LIGHT_MODEL_AMBIENT:
lighting_params.scene_ambient_color = { x, y, z, w };
break;
case GL_LIGHT_MODEL_COLOR_CONTROL: {
auto color_control = static_cast<GLenum>(x);
RETURN_WITH_ERROR_IF(color_control != GL_SINGLE_COLOR && color_control != GL_SEPARATE_SPECULAR_COLOR, GL_INVALID_ENUM);
lighting_params.color_control = (color_control == GL_SINGLE_COLOR) ? GPU::ColorControl::SingleColor : GPU::ColorControl::SeparateSpecularColor;
break;
}
case GL_LIGHT_MODEL_LOCAL_VIEWER:
// 0 means the viewer is at infinity
// 1 means they're in local (eye) space
lighting_params.viewer_at_infinity = (x == 0.f);
break;
case GL_LIGHT_MODEL_TWO_SIDE:
lighting_params.two_sided_lighting = (x != 0.f);
break;
default:
VERIFY_NOT_REACHED();
}
m_rasterizer->set_light_model_params(lighting_params);
}
void GLContext::gl_light_modelv(GLenum pname, void const* params, GLenum type)
{
auto invoke_implementation = [&](auto const* params) {
switch (pname) {
case GL_LIGHT_MODEL_AMBIENT:
gl_light_model(pname, params[0], params[1], params[2], params[3]);
return;
default:
gl_light_model(pname, params[0], 0.f, 0.f, 0.f);
return;
}
};
switch (type) {
case GL_FLOAT:
invoke_implementation(reinterpret_cast<GLfloat const*>(params));
break;
case GL_INT:
invoke_implementation(reinterpret_cast<GLint const*>(params));
break;
default:
VERIFY_NOT_REACHED();
}
}
void GLContext::gl_lightf(GLenum light, GLenum pname, GLfloat param)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_lightf, light, pname, param);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(light < GL_LIGHT0 || light >= (GL_LIGHT0 + m_device_info.num_lights), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_CONSTANT_ATTENUATION || pname == GL_LINEAR_ATTENUATION || pname == GL_QUADRATIC_ATTENUATION || pname != GL_SPOT_EXPONENT || pname != GL_SPOT_CUTOFF), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(param < 0.f, GL_INVALID_VALUE);
auto& light_state = m_light_states.at(light - GL_LIGHT0);
switch (pname) {
case GL_CONSTANT_ATTENUATION:
light_state.constant_attenuation = param;
break;
case GL_LINEAR_ATTENUATION:
light_state.linear_attenuation = param;
break;
case GL_QUADRATIC_ATTENUATION:
light_state.quadratic_attenuation = param;
break;
case GL_SPOT_EXPONENT:
RETURN_WITH_ERROR_IF(param > 128.f, GL_INVALID_VALUE);
light_state.spotlight_exponent = param;
break;
case GL_SPOT_CUTOFF:
RETURN_WITH_ERROR_IF(param > 90.f && param != 180.f, GL_INVALID_VALUE);
light_state.spotlight_cutoff_angle = param;
break;
default:
VERIFY_NOT_REACHED();
}
m_light_state_is_dirty = true;
}
void GLContext::gl_lightfv(GLenum light, GLenum pname, GLfloat const* params)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_lightfv, light, pname, params);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(light < GL_LIGHT0 || light >= (GL_LIGHT0 + m_device_info.num_lights), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_POSITION || pname == GL_CONSTANT_ATTENUATION || pname == GL_LINEAR_ATTENUATION || pname == GL_QUADRATIC_ATTENUATION || pname == GL_SPOT_CUTOFF || pname == GL_SPOT_EXPONENT || pname == GL_SPOT_DIRECTION), GL_INVALID_ENUM);
auto& light_state = m_light_states.at(light - GL_LIGHT0);
switch (pname) {
case GL_AMBIENT:
light_state.ambient_intensity = { params[0], params[1], params[2], params[3] };
break;
case GL_DIFFUSE:
light_state.diffuse_intensity = { params[0], params[1], params[2], params[3] };
break;
case GL_SPECULAR:
light_state.specular_intensity = { params[0], params[1], params[2], params[3] };
break;
case GL_POSITION:
light_state.position = { params[0], params[1], params[2], params[3] };
light_state.position = model_view_matrix() * light_state.position;
break;
case GL_CONSTANT_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0.f, GL_INVALID_VALUE);
light_state.constant_attenuation = params[0];
break;
case GL_LINEAR_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0.f, GL_INVALID_VALUE);
light_state.linear_attenuation = params[0];
break;
case GL_QUADRATIC_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0.f, GL_INVALID_VALUE);
light_state.quadratic_attenuation = params[0];
break;
case GL_SPOT_EXPONENT: {
auto exponent = params[0];
RETURN_WITH_ERROR_IF(exponent < 0.f || exponent > 128.f, GL_INVALID_VALUE);
light_state.spotlight_exponent = exponent;
break;
}
case GL_SPOT_CUTOFF: {
auto cutoff = params[0];
RETURN_WITH_ERROR_IF((cutoff < 0.f || cutoff > 90.f) && cutoff != 180.f, GL_INVALID_VALUE);
light_state.spotlight_cutoff_angle = cutoff;
break;
}
case GL_SPOT_DIRECTION: {
FloatVector4 direction_vector = { params[0], params[1], params[2], 0.f };
direction_vector = model_view_matrix() * direction_vector;
light_state.spotlight_direction = direction_vector.xyz();
break;
}
default:
VERIFY_NOT_REACHED();
}
m_light_state_is_dirty = true;
}
void GLContext::gl_lightiv(GLenum light, GLenum pname, GLint const* params)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_lightiv, light, pname, params);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(light < GL_LIGHT0 || light >= (GL_LIGHT0 + m_device_info.num_lights), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_POSITION || pname == GL_CONSTANT_ATTENUATION || pname == GL_LINEAR_ATTENUATION || pname == GL_QUADRATIC_ATTENUATION || pname == GL_SPOT_CUTOFF || pname == GL_SPOT_EXPONENT || pname == GL_SPOT_DIRECTION), GL_INVALID_ENUM);
auto& light_state = m_light_states[light - GL_LIGHT0];
auto const to_float_vector = [](GLfloat x, GLfloat y, GLfloat z, GLfloat w) {
return FloatVector4(x, y, z, w);
};
switch (pname) {
case GL_AMBIENT:
light_state.ambient_intensity = to_float_vector(params[0], params[1], params[2], params[3]);
break;
case GL_DIFFUSE:
light_state.diffuse_intensity = to_float_vector(params[0], params[1], params[2], params[3]);
break;
case GL_SPECULAR:
light_state.specular_intensity = to_float_vector(params[0], params[1], params[2], params[3]);
break;
case GL_POSITION:
light_state.position = to_float_vector(params[0], params[1], params[2], params[3]);
light_state.position = model_view_matrix() * light_state.position;
break;
case GL_CONSTANT_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0, GL_INVALID_VALUE);
light_state.constant_attenuation = static_cast<float>(params[0]);
break;
case GL_LINEAR_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0, GL_INVALID_VALUE);
light_state.linear_attenuation = static_cast<float>(params[0]);
break;
case GL_QUADRATIC_ATTENUATION:
RETURN_WITH_ERROR_IF(params[0] < 0, GL_INVALID_VALUE);
light_state.quadratic_attenuation = static_cast<float>(params[0]);
break;
case GL_SPOT_EXPONENT: {
auto exponent = static_cast<float>(params[0]);
RETURN_WITH_ERROR_IF(exponent < 0.f || exponent > 128.f, GL_INVALID_VALUE);
light_state.spotlight_exponent = exponent;
break;
}
case GL_SPOT_CUTOFF: {
auto cutoff = static_cast<float>(params[0]);
RETURN_WITH_ERROR_IF((cutoff < 0.f || cutoff > 90.f) && cutoff != 180.f, GL_INVALID_VALUE);
light_state.spotlight_cutoff_angle = cutoff;
break;
}
case GL_SPOT_DIRECTION: {
auto direction_vector = to_float_vector(params[0], params[1], params[2], 0.0f);
direction_vector = model_view_matrix() * direction_vector;
light_state.spotlight_direction = direction_vector.xyz();
break;
}
default:
VERIFY_NOT_REACHED();
}
m_light_state_is_dirty = true;
}
void GLContext::gl_materialf(GLenum face, GLenum pname, GLfloat param)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_materialf, face, pname, param);
RETURN_WITH_ERROR_IF(!(face == GL_FRONT || face == GL_BACK || face == GL_FRONT_AND_BACK), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(pname != GL_SHININESS, GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(param > 128.0f, GL_INVALID_VALUE);
switch (face) {
case GL_FRONT:
m_material_states[Face::Front].shininess = param;
break;
case GL_BACK:
m_material_states[Face::Back].shininess = param;
break;
case GL_FRONT_AND_BACK:
m_material_states[Face::Front].shininess = param;
m_material_states[Face::Back].shininess = param;
break;
default:
VERIFY_NOT_REACHED();
}
m_light_state_is_dirty = true;
}
void GLContext::gl_materialfv(GLenum face, GLenum pname, GLfloat const* params)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_materialfv, face, pname, params);
RETURN_WITH_ERROR_IF(!(face == GL_FRONT || face == GL_BACK || face == GL_FRONT_AND_BACK), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_EMISSION || pname == GL_SHININESS || pname == GL_AMBIENT_AND_DIFFUSE), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF((pname == GL_SHININESS && *params > 128.0f), GL_INVALID_VALUE);
auto update_material = [](GPU::Material& material, GLenum pname, GLfloat const* params) {
switch (pname) {
case GL_AMBIENT:
material.ambient = { params[0], params[1], params[2], params[3] };
break;
case GL_DIFFUSE:
material.diffuse = { params[0], params[1], params[2], params[3] };
break;
case GL_SPECULAR:
material.specular = { params[0], params[1], params[2], params[3] };
break;
case GL_EMISSION:
material.emissive = { params[0], params[1], params[2], params[3] };
break;
case GL_SHININESS:
material.shininess = params[0];
break;
case GL_AMBIENT_AND_DIFFUSE:
material.ambient = { params[0], params[1], params[2], params[3] };
material.diffuse = { params[0], params[1], params[2], params[3] };
break;
}
};
switch (face) {
case GL_FRONT:
update_material(m_material_states[Face::Front], pname, params);
break;
case GL_BACK:
update_material(m_material_states[Face::Back], pname, params);
break;
case GL_FRONT_AND_BACK:
update_material(m_material_states[Face::Front], pname, params);
update_material(m_material_states[Face::Back], pname, params);
break;
}
m_light_state_is_dirty = true;
}
void GLContext::gl_materialiv(GLenum face, GLenum pname, GLint const* params)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_materialiv, face, pname, params);
RETURN_WITH_ERROR_IF(!(face == GL_FRONT || face == GL_BACK || face == GL_FRONT_AND_BACK), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF(!(pname == GL_AMBIENT || pname == GL_DIFFUSE || pname == GL_SPECULAR || pname == GL_EMISSION || pname == GL_SHININESS || pname == GL_AMBIENT_AND_DIFFUSE), GL_INVALID_ENUM);
RETURN_WITH_ERROR_IF((pname == GL_SHININESS && *params > 128), GL_INVALID_VALUE);
auto update_material = [](GPU::Material& material, GLenum pname, GLint const* params) {
switch (pname) {
case GL_AMBIENT:
material.ambient = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
break;
case GL_DIFFUSE:
material.diffuse = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
break;
case GL_SPECULAR:
material.specular = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
break;
case GL_EMISSION:
material.emissive = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
break;
case GL_SHININESS:
material.shininess = static_cast<float>(params[0]);
break;
case GL_AMBIENT_AND_DIFFUSE:
material.ambient = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
material.diffuse = { static_cast<float>(params[0]), static_cast<float>(params[1]), static_cast<float>(params[2]), static_cast<float>(params[3]) };
break;
}
};
switch (face) {
case GL_FRONT:
update_material(m_material_states[Face::Front], pname, params);
break;
case GL_BACK:
update_material(m_material_states[Face::Back], pname, params);
break;
case GL_FRONT_AND_BACK:
update_material(m_material_states[Face::Front], pname, params);
update_material(m_material_states[Face::Back], pname, params);
break;
}
m_light_state_is_dirty = true;
}
void GLContext::gl_shade_model(GLenum mode)
{
APPEND_TO_CALL_LIST_AND_RETURN_IF_NEEDED(gl_shade_model, mode);
RETURN_WITH_ERROR_IF(m_in_draw_state, GL_INVALID_OPERATION);
RETURN_WITH_ERROR_IF(mode != GL_FLAT && mode != GL_SMOOTH, GL_INVALID_ENUM);
auto options = m_rasterizer->options();
options.shade_smooth = (mode == GL_SMOOTH);
m_rasterizer->set_options(options);
}
void GLContext::sync_light_state()
{
if (!m_light_state_is_dirty)
return;
m_light_state_is_dirty = false;
auto options = m_rasterizer->options();
options.color_material_enabled = m_color_material_enabled;
switch (m_color_material_face) {
case GL_BACK:
options.color_material_face = GPU::ColorMaterialFace::Back;
break;
case GL_FRONT:
options.color_material_face = GPU::ColorMaterialFace::Front;
break;
case GL_FRONT_AND_BACK:
options.color_material_face = GPU::ColorMaterialFace::FrontAndBack;
break;
default:
VERIFY_NOT_REACHED();
}
switch (m_color_material_mode) {
case GL_AMBIENT:
options.color_material_mode = GPU::ColorMaterialMode::Ambient;
break;
case GL_AMBIENT_AND_DIFFUSE:
options.color_material_mode = GPU::ColorMaterialMode::AmbientAndDiffuse;
break;
case GL_DIFFUSE:
options.color_material_mode = GPU::ColorMaterialMode::Diffuse;
break;
case GL_EMISSION:
options.color_material_mode = GPU::ColorMaterialMode::Emissive;
break;
case GL_SPECULAR:
options.color_material_mode = GPU::ColorMaterialMode::Specular;
break;
default:
VERIFY_NOT_REACHED();
}
m_rasterizer->set_options(options);
for (auto light_id = 0u; light_id < m_device_info.num_lights; light_id++) {
auto const& current_light_state = m_light_states.at(light_id);
m_rasterizer->set_light_state(light_id, current_light_state);
}
m_rasterizer->set_material_state(GPU::Face::Front, m_material_states[Face::Front]);
m_rasterizer->set_material_state(GPU::Face::Back, m_material_states[Face::Back]);
}
}