input: a controller's sub-device sensors, from SDL's left-hand sensors

The frontend passes a core's sensor calls for a sub-device
(RETRO_SENSOR_SUBDEVICE) on to the drivers with the index kept. It
remembers which sub-device sensors the core enabled, as it does the
controller's own, so one not enabled, or whose enable no driver took,
reads 0 without asking a driver. The controller's own sensors are
handled as before, and sensitivity and orientation apply to both; the
profile's axis remap stays the controller's.

The SDL2 (2.26 and up) and SDL3 joypad drivers answer sub-device 1 from
SDL's _L sensors: a Wii Remote's Nunchuk accelerometer, the left
Joy-Con of a pair. SDL3 restores it on a reconnect as it does the
others. Every other driver already refuses an index it does not know.

samples/input/sensor_subdevice runs the wrappers as they ship.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-28 16:37:08 +01:00
1 parent 04f8e90a9a
commit 80b3aefa35
9 files changed
+315 -12

No files matched your search

+13
View File
@@ -1124,6 +1124,19 @@ jobs:
timeout 120 ./udev_rumble_test
make clean
echo "[pass] udev_rumble_test"
- name: Build and run sensor_subdevice_test (ASan + UBSan)
shell: bash
working-directory: samples/input/sensor_subdevice
run: |
set -eu
# A core's sensor calls for a controller's sub-device (the
# Nunchuk, a left Joy-Con) through the frontend's wrappers as
# they ship: passed on with the index once enabled, refused
# when no driver has it, the controller's own unchanged.
make clean && make SANITIZER=address,undefined
UBSAN_OPTIONS=halt_on_error=1 timeout 60 ./sensor_subdevice_test
make clean
echo "[pass] sensor_subdevice_test"
- name: Build and run dingux_rumble_test (ASan + UBSan, then TSan)
shell: bash
working-directory: samples/input/dingux_rumble
+1
View File
@@ -393,3 +393,4 @@ samples/gfx/d3d12_upscaler/Zdir_xess/
samples/gfx/d3d12_upscaler/Zdir_none/
samples/record/ffmpeg_pipeline/record_finalize_fragment.c
samples/record/ffmpeg_pipeline/record_start_fragment.c
samples/input/sensor_subdevice/core_sensor_fragment.c
+41
View File
@@ -27,6 +27,8 @@
#define SDL_SUPPORTS_RUMBLE SDL_VERSION_ATLEAST(2, 0, 9)
#define SDL_SUPPORTS_SENSORS SDL_VERSION_ATLEAST(2, 0, 14)
/* the _L sensors: a Wii Remote's Nunchuk, the left Joy-Con of a pair */
#define SDL_SUPPORTS_SENSORS_L SDL_VERSION_ATLEAST(2, 26, 0)
#define SDL_SUPPORTS_TOUCHPAD SDL_VERSION_ATLEAST(2, 0, 14)
#define SDL_SUPPORTS_HIDAPI_WII SDL_VERSION_ATLEAST(2, 26, 0)
#define SDL_SUPPORTS_HIDAPI_PS3 SDL_VERSION_ATLEAST(2, 26, 0)
@@ -583,6 +585,31 @@ static bool sdl2_joypad_set_sensor_state(unsigned pad, enum retro_sensor_action
sdl2_joypad_t *joypad = (sdl2_joypad_t*)&sdl2_pads[pad];
(void)joypad; /* maybe unused */
/* sensor sub-device 1 (RETRO_SENSOR_SUBDEVICE): the part in the
* left hand */
if (RETRO_SENSOR_INDEX(action))
{
#if SDL_SUPPORTS_SENSORS_L
unsigned base = RETRO_SENSOR_BASE(action);
SDL_SensorType type;
if (RETRO_SENSOR_INDEX(action) != 1)
return false;
if ( base == RETRO_SENSOR_GYROSCOPE_ENABLE
|| base == RETRO_SENSOR_GYROSCOPE_DISABLE)
type = SDL_SENSOR_GYRO_L;
else if (base == RETRO_SENSOR_ACCELEROMETER_ENABLE
|| base == RETRO_SENSOR_ACCELEROMETER_DISABLE)
type = SDL_SENSOR_ACCEL_L;
else
return false;
if (SDL_GameControllerHasSensor(joypad->controller, type))
return !SDL_GameControllerSetSensorEnabled(joypad->controller, type,
(SDL_bool)( base == RETRO_SENSOR_GYROSCOPE_ENABLE
|| base == RETRO_SENSOR_ACCELEROMETER_ENABLE));
#endif
return false;
}
switch (action)
{
#if SDL_SUPPORTS_SENSORS
@@ -615,9 +642,18 @@ static bool sdl2_joypad_get_sensor_input(unsigned pad, unsigned id, float *value
sdl2_joypad_t *joypad = (sdl2_joypad_t*)&sdl2_pads[pad];
SDL_SensorType sensor_type;
float sensor_data[3];
unsigned idx = RETRO_SENSOR_INDEX(id);
if (!joypad->controller)
return false;
#if SDL_SUPPORTS_SENSORS_L
if (idx > 1)
return false;
#else
if (idx)
return false;
#endif
id = RETRO_SENSOR_BASE(id);
if ((id >= RETRO_SENSOR_ACCELEROMETER_X) && (id <= RETRO_SENSOR_ACCELEROMETER_Z))
sensor_type = SDL_SENSOR_ACCEL;
@@ -625,6 +661,11 @@ static bool sdl2_joypad_get_sensor_input(unsigned pad, unsigned id, float *value
sensor_type = SDL_SENSOR_GYRO;
else
return false;
#if SDL_SUPPORTS_SENSORS_L
if (idx)
sensor_type = sensor_type == SDL_SENSOR_ACCEL
? SDL_SENSOR_ACCEL_L : SDL_SENSOR_GYRO_L;
#endif
if (SDL_GameControllerGetSensorData(joypad->controller, sensor_type, sensor_data, 3) < 0)
return false;
+47 -6
View File
@@ -44,6 +44,11 @@ typedef struct _sdl3_joypad
uint16_t rumble[2]; /* raw magnitude per retro_rumble_effect (strong/weak) */
bool sensor_accel; /* Whether or not the sensor has been connected. */
bool sensor_gyro;
/* sensor sub-device 1 (RETRO_SENSOR_SUBDEVICE): the part in the left
* hand, a Wii Remote's Nunchuk or the left Joy-Con of a pair, which
* SDL reports as its _L sensors */
bool sensor_accel_l;
bool sensor_gyro_l;
uint8_t touch_fingers; /* the first touchpad's, at most two */
} sdl3_joypad_t;
@@ -359,6 +364,10 @@ static void sdl3_joypad_connect(SDL_JoystickID jid)
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL, true);
if (has_gyro && pad->sensor_gyro)
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO, true);
if (pad->sensor_accel_l && SDL_GamepadHasSensor(gamepad, SDL_SENSOR_ACCEL_L))
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_ACCEL_L, true);
if (pad->sensor_gyro_l && SDL_GamepadHasSensor(gamepad, SDL_SENSOR_GYRO_L))
SDL_SetGamepadSensorEnabled(gamepad, SDL_SENSOR_GYRO_L, true);
if (SDL_GetNumGamepadTouchpads(gamepad) > 0)
{
@@ -389,11 +398,15 @@ static void sdl3_joypad_disconnect(SDL_JoystickID jid)
/* Keep the requested sensor state so it can be restored on reconnect. */
{
bool sensor_accel = sdl3_joypads[i].sensor_accel;
bool sensor_gyro = sdl3_joypads[i].sensor_gyro;
bool sensor_accel = sdl3_joypads[i].sensor_accel;
bool sensor_gyro = sdl3_joypads[i].sensor_gyro;
bool sensor_accel_l = sdl3_joypads[i].sensor_accel_l;
bool sensor_gyro_l = sdl3_joypads[i].sensor_gyro_l;
memset(&sdl3_joypads[i], 0, sizeof(sdl3_joypads[i]));
sdl3_joypads[i].sensor_accel = sensor_accel;
sdl3_joypads[i].sensor_gyro = sensor_gyro;
sdl3_joypads[i].sensor_accel = sensor_accel;
sdl3_joypads[i].sensor_gyro = sensor_gyro;
sdl3_joypads[i].sensor_accel_l = sensor_accel_l;
sdl3_joypads[i].sensor_gyro_l = sensor_gyro_l;
}
return;
}
@@ -717,6 +730,28 @@ static bool sdl3_joypad_set_sensor_state(unsigned pad,
gamepad = sdl3_joypads[pad].gamepad;
if (RETRO_SENSOR_INDEX(action) == 1)
{
unsigned base = RETRO_SENSOR_BASE(action);
bool gyro = base == RETRO_SENSOR_GYROSCOPE_ENABLE
|| base == RETRO_SENSOR_GYROSCOPE_DISABLE;
bool enable = base == RETRO_SENSOR_GYROSCOPE_ENABLE
|| base == RETRO_SENSOR_ACCELEROMETER_ENABLE;
SDL_SensorType type = gyro ? SDL_SENSOR_GYRO_L : SDL_SENSOR_ACCEL_L;
if ( !gyro
&& base != RETRO_SENSOR_ACCELEROMETER_ENABLE
&& base != RETRO_SENSOR_ACCELEROMETER_DISABLE)
return false;
if (gyro)
sdl3_joypads[pad].sensor_gyro_l = enable;
else
sdl3_joypads[pad].sensor_accel_l = enable;
if (gamepad && SDL_GamepadHasSensor(gamepad, type))
return SDL_SetGamepadSensorEnabled(gamepad, type, enable);
return !enable;
}
switch (action)
{
case RETRO_SENSOR_GYROSCOPE_ENABLE:
@@ -759,6 +794,7 @@ static bool sdl3_joypad_get_sensor_input(unsigned pad, unsigned id, float *value
{
SDL_SensorType sensor_type;
float sensor_data[3];
bool left;
if (pad >= MAX_USERS)
return false;
@@ -766,10 +802,15 @@ static bool sdl3_joypad_get_sensor_input(unsigned pad, unsigned id, float *value
if (!sdl3_joypads[pad].gamepad)
return false;
if (RETRO_SENSOR_INDEX(id) > 1)
return false;
left = RETRO_SENSOR_INDEX(id) == 1;
id = RETRO_SENSOR_BASE(id);
if ((id >= RETRO_SENSOR_ACCELEROMETER_X) && (id <= RETRO_SENSOR_ACCELEROMETER_Z))
sensor_type = SDL_SENSOR_ACCEL;
sensor_type = left ? SDL_SENSOR_ACCEL_L : SDL_SENSOR_ACCEL;
else if ((id >= RETRO_SENSOR_GYROSCOPE_X) && (id <= RETRO_SENSOR_GYROSCOPE_Z))
sensor_type = SDL_SENSOR_GYRO;
sensor_type = left ? SDL_SENSOR_GYRO_L : SDL_SENSOR_GYRO;
else
return false;
+67 -6
View File
@@ -1912,10 +1912,11 @@ bool input_driver_set_sensor(
{
const input_driver_t *current_driver;
bool enabled = false;
unsigned base = RETRO_SENSOR_BASE(action);
bool is_disable =
(action == RETRO_SENSOR_ACCELEROMETER_DISABLE)
|| (action == RETRO_SENSOR_GYROSCOPE_DISABLE)
|| (action == RETRO_SENSOR_ILLUMINANCE_DISABLE);
(base == RETRO_SENSOR_ACCELEROMETER_DISABLE)
|| (base == RETRO_SENSOR_GYROSCOPE_DISABLE)
|| (base == RETRO_SENSOR_ILLUMINANCE_DISABLE);
if (!input_driver_st.current_data)
return false;
@@ -8680,6 +8681,7 @@ bool input_set_sensor_state(unsigned port,
joy_idx, input_sensors_enable, action, rate);
}
/* ===== BEGIN input core sensor wrappers ===== */
/* Core-facing wrappers that track which sensors the core has enabled.
* Used as the retro_sensor_interface callbacks so the frontend knows
* what the core requested independently of shader/platform enables. */
@@ -8687,6 +8689,39 @@ bool input_core_set_sensor_state(unsigned port,
enum retro_sensor_action action, unsigned rate)
{
input_driver_state_t *input_st = &input_driver_st;
unsigned idx = RETRO_SENSOR_INDEX(action);
/* A sub-device's sensor (RETRO_SENSOR_SUBDEVICE): passed on as it
* came, for a joypad driver that knows the controller's parts */
if (idx)
{
uint8_t bit;
if (idx > INPUT_SENSOR_SUBDEVICES)
return false;
switch (RETRO_SENSOR_BASE(action))
{
case RETRO_SENSOR_ACCELEROMETER_ENABLE:
case RETRO_SENSOR_ACCELEROMETER_DISABLE:
bit = (uint8_t)(1u << (2 * (idx - 1)));
break;
case RETRO_SENSOR_GYROSCOPE_ENABLE:
case RETRO_SENSOR_GYROSCOPE_DISABLE:
bit = (uint8_t)(2u << (2 * (idx - 1)));
break;
default:
return false;
}
if ( RETRO_SENSOR_BASE(action) == RETRO_SENSOR_ACCELEROMETER_ENABLE
|| RETRO_SENSOR_BASE(action) == RETRO_SENSOR_GYROSCOPE_ENABLE)
{
if (!input_set_sensor_state(port, action, rate))
return false;
input_st->core_sub_sensors |= bit;
return true;
}
input_st->core_sub_sensors &= (uint8_t)~bit;
return input_set_sensor_state(port, action, rate);
}
switch (action)
{
@@ -8713,6 +8748,24 @@ bool input_core_set_sensor_state(unsigned port,
float input_core_get_sensor_state(unsigned port, unsigned id)
{
input_driver_state_t *input_st = &input_driver_st;
unsigned idx = RETRO_SENSOR_INDEX(id);
if (idx)
{
unsigned base = RETRO_SENSOR_BASE(id);
unsigned bit;
if (idx > INPUT_SENSOR_SUBDEVICES)
return 0.0f;
if (base <= RETRO_SENSOR_ACCELEROMETER_Z)
bit = 1u << (2 * (idx - 1));
else if (base <= RETRO_SENSOR_GYROSCOPE_Z)
bit = 2u << (2 * (idx - 1));
else
return 0.0f;
if (!(input_st->core_sub_sensors & bit))
return 0.0f;
return input_get_sensor_state(port, id);
}
/* Only return data for sensor types the core has enabled */
if (id <= RETRO_SENSOR_ACCELEROMETER_Z && !input_st->core_accel_rate)
@@ -8724,6 +8777,7 @@ float input_core_get_sensor_state(unsigned port, unsigned id)
return input_get_sensor_state(port, id);
}
/* ===== END input core sensor wrappers ===== */
const char *joypad_driver_name(unsigned i)
{
@@ -8845,9 +8899,15 @@ static float input_get_sensor_state_internal(
unsigned joy_idx;
bool input_sensors_enable = false;
float sensitivity = 1.0f;
unsigned fetch_id = id;
/* a sub-device's sensor is turned and scaled as the controller's;
* the profile's axis remap is the controller's own */
unsigned idx = RETRO_SENSOR_INDEX(id);
unsigned fetch_id;
float sign = 1.0f;
id = RETRO_SENSOR_BASE(id);
fetch_id = id;
/* Return 0 if settings unavailable (e.g., during driver transitions) */
if (!settings)
return 0.0f;
@@ -8863,7 +8923,7 @@ static float input_get_sensor_state_internal(
return 0.0f;
/* Apply sensor axis remap from autoconfig profile */
if (id <= RETRO_SENSOR_GYROSCOPE_Z)
if (!idx && id <= RETRO_SENSOR_GYROSCOPE_Z)
{
const input_sensor_map_t *map = input_config_get_sensor_map(joy_idx);
if (map && map->axes[id].source >= 0)
@@ -8942,7 +9002,8 @@ static float input_get_sensor_state_internal(
#endif /* HAVE_COCOATOUCH */
/* Get raw sensor value (use fetch_id for rotated axes) */
raw_value = input_driver_get_sensor(joy_idx, true, fetch_id);
raw_value = input_driver_get_sensor(joy_idx, true,
RETRO_SENSOR_SUBDEVICE(idx, fetch_id));
/* Apply sensitivity and rotation sign */
return raw_value * sensitivity * sign;
+6
View File
@@ -618,6 +618,9 @@ struct input_driver
* changes only in its own poll: lent to the readers between two polls
* in place of a copy (input_driver.c, the snapshot bridge). What each
* field holds is what button(), axis() and get_buttons() answer. */
/* sensor sub-devices a core may address (RETRO_SENSOR_SUBDEVICE),
* after the controller itself */
#define INPUT_SENSOR_SUBDEVICES 4
#define INPUT_PAD_VIEW_AXES 16
#define INPUT_PAD_VIEW_HATS 4
#define INPUT_PAD_VIEW_TOUCHES 2
@@ -1021,6 +1024,9 @@ typedef struct
bool platform_menu_button;
unsigned core_accel_rate; /* >0 means core wants accel at this rate */
unsigned core_gyro_rate; /* >0 means core wants gyro at this rate */
/* the sub-devices' sensors the core has enabled: bit 2(i-1) the
* accelerometer of sub-device i, bit 2(i-1)+1 its gyroscope */
uint8_t core_sub_sensors;
/* First-press port assignment, see input_first_press_apply():
* buttons seen released on a user with no core port, the users to
* map at the next frame boundary, and whether any of it is live. */
+1
View File
@@ -5724,6 +5724,7 @@ void runloop_event_deinit_core(void)
input_driver_state_t *input_st = input_state_get_ptr();
input_st->core_accel_rate = 0;
input_st->core_gyro_rate = 0;
input_st->core_sub_sensors = 0;
}
video_driver_cached_frame_retire();
+21
View File
@@ -0,0 +1,21 @@
ROOT := ../../..
TARGET := sensor_subdevice_test
CC ?= cc
# The frontend's core sensor wrappers, extracted from input_driver.c
# between their markers, so the test runs the shipping code.
CFLAGS += -std=gnu99 -O1 -g1 -Wall -DRARCH_INTERNAL \
-I. -I$(ROOT) -I$(ROOT)/libretro-common/include -I$(ROOT)/deps
ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
all: $(TARGET)
core_sensor_fragment.c: $(ROOT)/input/input_driver.c
sed -n '/===== BEGIN input core sensor wrappers =====/,/===== END input core sensor wrappers =====/p' $< > $@
$(TARGET): sensor_subdevice_test.c core_sensor_fragment.c
$(CC) $(CFLAGS) -o $@ sensor_subdevice_test.c $(LDFLAGS)
check: all
./$(TARGET)
clean:
rm -f $(TARGET) $(TARGET).exe core_sensor_fragment.c
.PHONY: all check clean
@@ -0,0 +1,118 @@
/* A core's sensor calls with a sub-device index (RETRO_SENSOR_SUBDEVICE),
* through the frontend's wrappers as they ship (extracted from
* input_driver.c between their markers). The controller's own sensors
* behave as before; a sub-device's are passed on to the drivers with
* the index kept, read only once enabled, and refused past the
* sub-devices the frontend tracks. */
#include <stdio.h>
#include <string.h>
#include <boolean.h>
#include <libretro.h>
#include "input/input_driver.h"
static input_driver_state_t input_driver_st;
/* the driver side, stood in for: what it was asked, and its answer */
static unsigned last_action, last_id, set_calls, get_calls;
static bool driver_has = true;
bool input_set_sensor_state(unsigned port,
enum retro_sensor_action action, unsigned rate)
{
(void)port; (void)rate;
last_action = (unsigned)action;
set_calls++;
return driver_has;
}
float input_get_sensor_state(unsigned port, unsigned id)
{
(void)port;
last_id = id;
get_calls++;
return 0.5f;
}
#include "core_sensor_fragment.c"
static unsigned failures;
#define CHECK(cond, msg) do { if (!(cond)) { \
printf(" FAIL %s\n", msg); failures++; } } while (0)
#define SUB(i, x) ((enum retro_sensor_action)RETRO_SENSOR_SUBDEVICE((i), (x)))
int main(void)
{
/* the controller's own: as before */
CHECK(RETRO_SENSOR_SUBDEVICE(0, RETRO_SENSOR_GYROSCOPE_Y) == RETRO_SENSOR_GYROSCOPE_Y,
"index 0 is not the value in use");
CHECK(input_core_get_sensor_state(0, RETRO_SENSOR_ACCELEROMETER_X) == 0.0f,
"the accelerometer read before it was enabled");
CHECK(input_core_set_sensor_state(0, RETRO_SENSOR_ACCELEROMETER_ENABLE, 60)
&& input_driver_st.core_accel_rate == 60, "the accelerometer enable");
CHECK(input_core_get_sensor_state(0, RETRO_SENSOR_ACCELEROMETER_X) == 0.5f
&& last_id == RETRO_SENSOR_ACCELEROMETER_X, "the accelerometer read");
printf(" ok the controller's own sensors behave as before\n");
/* the Nunchuk's accelerometer: refused while not enabled, passed
* on with its index once it is */
get_calls = 0;
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_ACCELEROMETER_X)) == 0.0f
&& get_calls == 0, "sub-device 1 read before it was enabled");
CHECK(input_core_set_sensor_state(0, SUB(1, RETRO_SENSOR_ACCELEROMETER_ENABLE), 100)
&& last_action == RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_ACCELEROMETER_ENABLE),
"sub-device 1's enable was not passed on with its index");
CHECK(input_driver_st.core_accel_rate == 60,
"sub-device 1's enable changed the controller's rate");
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_ACCELEROMETER_Z)) == 0.5f
&& last_id == RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_ACCELEROMETER_Z),
"sub-device 1's read was not passed on with its index");
get_calls = 0;
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_GYROSCOPE_X)) == 0.0f
&& get_calls == 0, "sub-device 1's gyroscope read without being enabled");
printf(" ok a sub-device's sensor is passed on with its index, once enabled\n");
/* disabled: no more reads */
CHECK(input_core_set_sensor_state(0, SUB(1, RETRO_SENSOR_ACCELEROMETER_DISABLE), 0),
"sub-device 1's disable");
get_calls = 0;
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_ACCELEROMETER_X)) == 0.0f
&& get_calls == 0, "sub-device 1 read after it was disabled");
/* a frontend driver without it: the enable says so, and nothing reads */
driver_has = false;
CHECK(!input_core_set_sensor_state(0, SUB(1, RETRO_SENSOR_GYROSCOPE_ENABLE), 100),
"a sub-device the drivers do not have was enabled");
get_calls = 0;
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(1, RETRO_SENSOR_GYROSCOPE_X)) == 0.0f
&& get_calls == 0, "a sub-device that failed to enable was read");
driver_has = true;
/* past what is tracked, and actions with no sensor behind them */
set_calls = 0;
CHECK(!input_core_set_sensor_state(0,
SUB(INPUT_SENSOR_SUBDEVICES + 1, RETRO_SENSOR_ACCELEROMETER_ENABLE), 100)
&& set_calls == 0, "a sub-device past the tracked ones was passed on");
CHECK(!input_core_set_sensor_state(0, SUB(1, RETRO_SENSOR_ILLUMINANCE_ENABLE), 100)
&& set_calls == 0, "a sub-device's light sensor was passed on");
CHECK(input_core_get_sensor_state(0,
RETRO_SENSOR_SUBDEVICE(INPUT_SENSOR_SUBDEVICES + 1,
RETRO_SENSOR_ACCELEROMETER_X)) == 0.0f,
"a sub-device past the tracked ones read");
printf(" ok disabled, missing and untracked sub-devices read 0\n");
if (failures)
{
printf("FAIL sensor_subdevice_test: %u\n", failures);
return 1;
}
printf("PASS sensor_subdevice_test\n");
return 0;
}