diff options
Diffstat (limited to 'src/devices/bus/sms_ctrl/lphaser.cpp')
-rw-r--r-- | src/devices/bus/sms_ctrl/lphaser.cpp | 255 |
1 files changed, 127 insertions, 128 deletions
diff --git a/src/devices/bus/sms_ctrl/lphaser.cpp b/src/devices/bus/sms_ctrl/lphaser.cpp index 2dead793887..a008b081f6d 100644 --- a/src/devices/bus/sms_ctrl/lphaser.cpp +++ b/src/devices/bus/sms_ctrl/lphaser.cpp @@ -19,47 +19,63 @@ Notes: **********************************************************************/ #include "emu.h" -#include "screen.h" #include "lphaser.h" +#include "screen.h" + +//#define VERBOSE 1 +#include "logmacro.h" +namespace { + //************************************************************************** -// DEVICE DEFINITIONS +// TYPE DEFINITIONS //************************************************************************** -DEFINE_DEVICE_TYPE(SMS_LIGHT_PHASER, sms_light_phaser_device, "sms_light_phaser", "Sega SMS Light Phaser") +class sms_light_phaser_device : public device_t, public device_sms_control_interface +{ +public: + // construction/destruction + sms_light_phaser_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock); + + DECLARE_INPUT_CHANGED_MEMBER(position_changed) { sensor_check(0); } + // device_sms_control_interface implementation + virtual uint8_t in_r() override { return m_trigger->read(); } -#define LGUN_RADIUS 6 -#define LGUN_X_INTERVAL 4 +protected: + // device_t implementation + virtual ioport_constructor device_input_ports() const override ATTR_COLD; + virtual void device_start() override ATTR_COLD; + virtual void device_reset() override ATTR_COLD; +private: + required_ioport m_trigger; + required_ioport m_x_axis; + required_ioport m_y_axis; -READ_LINE_MEMBER( sms_light_phaser_device::th_pin_r ) -{ - // The returned value is inverted due to IP_ACTIVE_LOW mapping. - return ~m_sensor_last_state; -} + emu_timer *m_update_timer; + int m_sensor_last_state; -INPUT_CHANGED_MEMBER( sms_light_phaser_device::position_changed ) -{ - if (newval != oldval) - sensor_check(); -} + TIMER_CALLBACK_MEMBER(sensor_check); + int bright_aim_area(int lgun_x, int lgun_y ); + uint16_t screen_hpos_nonscaled(int scaled_hpos); + uint16_t screen_vpos_nonscaled(int scaled_vpos); +}; -static INPUT_PORTS_START( sms_light_phaser ) - PORT_START("CTRL_PORT") - PORT_BIT( 0x20, IP_ACTIVE_LOW, IPT_BUTTON1 ) // TL (trigger) - PORT_BIT( 0x40, IP_ACTIVE_LOW, IPT_CUSTOM ) PORT_READ_LINE_DEVICE_MEMBER(DEVICE_SELF, sms_light_phaser_device, th_pin_r) - PORT_BIT( 0x9f, IP_ACTIVE_LOW, IPT_UNUSED ) +INPUT_PORTS_START(sms_light_phaser) + PORT_START("LPHASER_T") + PORT_BIT( 0x10, IP_ACTIVE_LOW, IPT_BUTTON1 ) // TL (trigger) + PORT_BIT( 0x2f, IP_ACTIVE_LOW, IPT_UNUSED ) PORT_START("LPHASER_X") - PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_X) PORT_CROSSHAIR(X, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15) PORT_CHANGED_MEMBER(DEVICE_SELF, sms_light_phaser_device, position_changed, 0) + PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_X) PORT_CROSSHAIR(X, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sms_light_phaser_device::position_changed), 0) PORT_START("LPHASER_Y") - PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_Y) PORT_CROSSHAIR(Y, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15) PORT_CHANGED_MEMBER(DEVICE_SELF, sms_light_phaser_device, position_changed, 0) + PORT_BIT( 0xff, 0x00, IPT_LIGHTGUN_Y) PORT_CROSSHAIR(Y, 1.0, 0.0, 0) PORT_SENSITIVITY(50) PORT_KEYDELTA(15) PORT_CHANGED_MEMBER(DEVICE_SELF, FUNC(sms_light_phaser_device::position_changed), 0) INPUT_PORTS_END @@ -69,25 +85,22 @@ INPUT_PORTS_END ioport_constructor sms_light_phaser_device::device_input_ports() const { - return INPUT_PORTS_NAME( sms_light_phaser ); + return INPUT_PORTS_NAME(sms_light_phaser); } - -//************************************************************************** -// LIVE DEVICE -//************************************************************************** - //------------------------------------------------- // sms_light_phaser_device - constructor //------------------------------------------------- sms_light_phaser_device::sms_light_phaser_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : device_t(mconfig, SMS_LIGHT_PHASER, tag, owner, clock), - device_sms_control_port_interface(mconfig, *this), - m_lphaser_pins(*this, "CTRL_PORT"), - m_lphaser_x(*this, "LPHASER_X"), - m_lphaser_y(*this, "LPHASER_Y"), m_sensor_last_state(0), m_lphaser_timer(nullptr) + device_sms_control_interface(mconfig, *this), + m_trigger(*this, "LPHASER_T"), + m_x_axis(*this, "LPHASER_X"), + m_y_axis(*this, "LPHASER_Y"), + m_update_timer(nullptr), + m_sensor_last_state(0) { } @@ -98,61 +111,61 @@ sms_light_phaser_device::sms_light_phaser_device(const machine_config &mconfig, void sms_light_phaser_device::device_start() { - save_item(NAME(m_sensor_last_state)); - m_lphaser_timer = timer_alloc(TIMER_LPHASER); -} + m_update_timer = timer_alloc(FUNC(sms_light_phaser_device::sensor_check), this); + m_sensor_last_state = 1; -void sms_light_phaser_device::device_reset() -{ - m_sensor_last_state = 1; // off (1) + save_item(NAME(m_sensor_last_state)); } -//------------------------------------------------- -// sms_peripheral_r - light phaser read -//------------------------------------------------- - -uint8_t sms_light_phaser_device::peripheral_r() +void sms_light_phaser_device::device_reset() { - return m_lphaser_pins->read(); + // TODO: sensor check now? The device itself doesn't respond to system reset } /* Light Phaser (light gun) emulation notes: - - The sensor is activated based on color brightness of some individual - pixels being drawn by the beam, at circular area where the gun is aiming. + - The sensor is activated based on the brightness of individual pixels being + drawn by the beam, in circular area where the gun is aiming. - Currently, brightness is calculated based only on single pixels. - In general, after the trigger is pressed, games draw the next frame using - a light color pattern, to make sure sensor will be activated. If emulation - skips that frame, sensor may stay deactivated. Frameskip set to 0 (no skip) + a bright pattern, to make sure the sensor will be activated. If the emulation + skips that frame, the sensor may stay deactivated. Setting frameskip to 0 is recommended to avoid problems. - - When sensor switches from on (0) to off (1), a value is latched for the + - When the sensor switches from on (0) to off (1), a value is latched for the HCount register. - - When sensor switches from off to on, a flag is set. The emulation uses the - flag to signal that TH line is activated when the status of the input port - is read. After read, the flag is cleared, or else it is cleared later when - the Pause status is read (end of a frame). This is necessary because the + - When the sensor switches from off to on, a flag is set. The emulation uses + this flag to signal that the TH line is activated during status reads of the + input port. After reading, the flag is cleared, or it is cleared when the + Pause status is read at the end of a frame. This is necessary because the "Color & Switch Test" ROM only reads the TH state after VINT occurs. - - The gun test of "Color & Switch Test" is an example that requires checks - of sensor status independent of other events, like trigger press or TH bit + - The gun test in "Color & Switch Test" is an example that requires checks + of the sensor status independent of other events, like trigger press or TH bit reads. Another example is the title screen of "Hang-On & Safari Hunt", where - the game only reads HCount register in a loop, expecting a latch by the gun. - - The whole procedure is managed by a timer callback, that always reschedule - itself to run in some intervals when the beam is at the circular area. + the game only reads the HCount register in a loop, expecting a latch by the gun. + - The whole procedure is managed by a periodic timer callback, which is scheduled + to run in intervals when the beam is within the circular area. */ -int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int lgun_y ) +int sms_light_phaser_device::bright_aim_area(int lgun_x, int lgun_y) { + + constexpr int LGUN_RADIUS = 6; + constexpr int LGUN_X_INTERVAL = 4; + + // brightness of the lightgray color in the frame drawn by Light Phaser games + constexpr uint8_t SENSOR_MIN_BRIGHTNESS = 0x7f; + + screen_device &scr(*screen()); const int r_x_r = LGUN_RADIUS * LGUN_RADIUS; - const rectangle &visarea = m_port->m_screen->visible_area(); + const rectangle &visarea = scr.visible_area(); rectangle aim_area; - int beam_x = m_port->m_screen->hpos(); - int beam_y = m_port->m_screen->vpos(); - int beam_x_orig = beam_x; - int beam_y_orig = beam_y; - int dy, result = 1; - double dx_radius; + int beam_x = scr.hpos(); + int beam_y = scr.vpos(); + const int beam_x_orig = beam_x; + const int beam_y_orig = beam_y; + int result = 1; bool new_check_point = false; aim_area.min_y = std::max(lgun_y - LGUN_RADIUS, visarea.min_y); @@ -160,29 +173,25 @@ int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int while (!new_check_point) { - /* If beam's y doesn't point to a line where the aim area is, - change it to the first line where the beam enters that area. */ + // If beam's y doesn't point to a line where the aim area is, change it to the first line where the beam enters that area. if (beam_y < aim_area.min_y || beam_y > aim_area.max_y) { beam_y = aim_area.min_y; } - dy = abs(beam_y - lgun_y); + int dy = abs(beam_y - lgun_y); - /* Caculate distance in x of the radius, relative to beam's y distance. - First try some shortcuts. */ - switch (dy) + // Calculate distance in x of the radius, relative to beam's y distance. First try some shortcuts. + double dx_radius = 0; + if (dy == 0) { - case LGUN_RADIUS: - dx_radius = 0; - break; - case 0: dx_radius = LGUN_RADIUS; - break; - default: - /* step 1: r^2 = dx^2 + dy^2 */ - /* step 2: dx^2 = r^2 - dy^2 */ - /* step 3: dx = sqrt(r^2 - dy^2) */ - dx_radius = ceil((float) sqrt((float) (r_x_r - (dy * dy)))); + } + else if (dy != LGUN_RADIUS) + { + // step 1: r^2 = dx^2 + dy^2 + // step 2: dx^2 = r^2 - dy^2 + // step 3: dx = sqrt(r^2 - dy^2) + dx_radius = ceil(sqrt(double(r_x_r - (dy * dy)))); } aim_area.min_x = std::max(int32_t(lgun_x - dx_radius), visarea.min_x); @@ -190,8 +199,7 @@ int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int while (!new_check_point) { - /* If beam's x has passed the aim area, change it to the - next line and go back to recheck y/x coordinates. */ + // If beam's x has passed the aim area, change it to the next line and go back to recheck y/x coordinates. if (beam_x > aim_area.max_x) { beam_x = visarea.min_x; @@ -199,8 +207,7 @@ int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int break; } - /* If beam's x isn't in the aim area, change it to the - next point where the beam enters that area. */ + // If beam's x isn't in the aim area, change it to the next point where the beam enters that area. if (beam_x < aim_area.min_x) { beam_x = aim_area.min_x; @@ -208,58 +215,52 @@ int sms_light_phaser_device::bright_aim_area( emu_timer *timer, int lgun_x, int if (beam_x_orig != beam_x || beam_y_orig != beam_y) { - /* adopt the new coordinates to adjust the timer */ + // adopt the new coordinates to adjust the timer new_check_point = true; break; } if (m_sensor_last_state == 0) { - /* sensor is already on */ - /* keep sensor on until out of the aim area */ + // sensor is already on - keep sensor on until out of the aim area result = 0; } else { - rgb_t color; - uint8_t brightness; - /* brightness of the lightgray color in the frame drawn by Light Phaser games */ - const uint8_t sensor_min_brightness = 0x7f; + scr.update_now(); + const rgb_t color = scr.pixel(beam_x, beam_y); - m_port->m_screen->update_now(); - color = m_port->m_screen->pixel(beam_x, beam_y); + // reference: http://www.w3.org/TR/AERT#color-contrast + const uint8_t brightness = (color.r() * 0.299) + (color.g() * 0.587) + (color.b() * 0.114); + LOG("color brightness: %2X for x %d y %d\n", brightness, beam_x, beam_y); - /* reference: http://www.w3.org/TR/AERT#color-contrast */ - brightness = (color.r() * 0.299) + (color.g() * 0.587) + (color.b() * 0.114); - //printf ("color brightness: %2X for x %d y %d\n", brightness, beam_x, beam_y); - - result = (brightness >= sensor_min_brightness) ? 0 : 1; + result = (brightness >= SENSOR_MIN_BRIGHTNESS) ? 0 : 1; } - if (result == 0) /* sensor on */ + if (result == 0) // sensor on { - /* Set next check for when sensor will be off */ + // Set next check for when sensor will be off beam_x = aim_area.max_x + 1; - /* adopt the new coordinates to adjust the timer */ + // adopt the new coordinates to adjust the timer new_check_point = true; } else { - /* Next check will happen after the minimum interval */ + // Next check will happen after the minimum interval beam_x += LGUN_X_INTERVAL; } } } - timer->adjust(m_port->m_screen->time_until_pos(beam_y, beam_x)); + m_update_timer->adjust(scr.time_until_pos(beam_y, beam_x)); return result; } uint16_t sms_light_phaser_device::screen_hpos_nonscaled(int scaled_hpos) { - const rectangle &visarea = m_port->m_screen->visible_area(); + const rectangle &visarea = screen()->visible_area(); int offset_x = (scaled_hpos * (visarea.max_x - visarea.min_x)) / 255; return visarea.min_x + offset_x; } @@ -267,36 +268,34 @@ uint16_t sms_light_phaser_device::screen_hpos_nonscaled(int scaled_hpos) uint16_t sms_light_phaser_device::screen_vpos_nonscaled(int scaled_vpos) { - const rectangle &visarea = m_port->m_screen->visible_area(); + const rectangle &visarea = screen()->visible_area(); int offset_y = (scaled_vpos * (visarea.max_y - visarea.min_y)) / 255; return visarea.min_y + offset_y; } -void sms_light_phaser_device::sensor_check() +TIMER_CALLBACK_MEMBER(sms_light_phaser_device::sensor_check) { - int sensor_new_state; - - const int x = screen_hpos_nonscaled(m_lphaser_x->read()); - const int y = screen_vpos_nonscaled(m_lphaser_y->read()); - - sensor_new_state = bright_aim_area(m_lphaser_timer, x, y); - if (sensor_new_state != m_sensor_last_state) + if (screen()) { - m_port->th_pin_w(sensor_new_state); - m_sensor_last_state = sensor_new_state; + const int x = screen_hpos_nonscaled(m_x_axis->read()); + const int y = screen_vpos_nonscaled(m_y_axis->read()); + + int sensor_new_state = bright_aim_area(x, y); + if (sensor_new_state != m_sensor_last_state) + { + th_w(sensor_new_state); + m_sensor_last_state = sensor_new_state; + } } } +} // anonymous namespace -void sms_light_phaser_device::device_timer(emu_timer &timer, device_timer_id id, int param, void *ptr) -{ - switch (id) - { - case TIMER_LPHASER: - sensor_check(); - break; - default: - throw emu_fatalerror("sms_light_phaser_device(%s): Unknown timer ID", tag()); - } -} + + +//************************************************************************** +// DEVICE DEFINITIONS +//************************************************************************** + +DEFINE_DEVICE_TYPE_PRIVATE(SMS_LIGHT_PHASER, device_sms_control_interface, sms_light_phaser_device, "sms_light_phaser", "Sega Master System Light Phaser") |