diff options
Diffstat (limited to 'src/devices/video/pwm.cpp')
-rw-r--r-- | src/devices/video/pwm.cpp | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/src/devices/video/pwm.cpp b/src/devices/video/pwm.cpp new file mode 100644 index 00000000000..c54cdbc979a --- /dev/null +++ b/src/devices/video/pwm.cpp @@ -0,0 +1,269 @@ +// license:BSD-3-Clause +// copyright-holders:hap +/* + +This thing is a generic helper for PWM(strobed) display elements, to prevent flickering +and optionally handle perceived brightness levels. + +Common usecase is to call matrix(selmask, datamask), a collision between the 2 masks +implies a powered-on display element (eg, a LED, or VFD sprite). The maximum matrix +size is 64 by 64, simply due to uint64_t constraints. If a larger size is needed, +create an array of pwm_display_device. + +If display elements are directly addressable, you can use write_element or write_row +to set them. In this case it is required to call update() to apply the changes. + +Display element states are sent to output tags "y.x" where y is the matrix row number, +x is the row bit. It is also sent to "y.a" for all rows. The output state is 0 for off, +and >0 for on, depending on brightness level. If segmask is defined, it is also sent +to "digity", for use with multi-state elements, eg. 7seg leds. + +If you use this device in a slot, or use multiple of them (or just don't want to use +the default output tags), set a callback. + +TODO: +- multiple brightness levels doesn't work for SVGs + +*/ + +#include "emu.h" +#include "video/pwm.h" + +#include <algorithm> + + +DEFINE_DEVICE_TYPE(PWM_DISPLAY, pwm_display_device, "pwm_display", "PWM Display") + +//------------------------------------------------- +// constructor +//------------------------------------------------- + +pwm_display_device::pwm_display_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) : + device_t(mconfig, PWM_DISPLAY, tag, owner, clock), + m_out_x(*this, "%u.%u", 0U, 0U), + m_out_a(*this, "%u.a", 0U), + m_out_digit(*this, "digit%u", 0U), + m_output_x_cb(*this), + m_output_a_cb(*this), + m_output_digit_cb(*this) +{ + // set defaults (60hz frames, 0.5 interpolation, 1 brightness level) + set_refresh(attotime::from_hz(60)); + set_interpolation(0.5); + set_bri_levels(0.02); + set_bri_minimum(0); + set_size(0, 0); + reset_segmask(); +} + + + +//------------------------------------------------- +// device_start/reset +//------------------------------------------------- + +ALLOW_SAVE_TYPE(attotime); // m_acc + +void pwm_display_device::device_start() +{ + // resolve handlers + m_out_x.resolve(); + m_out_a.resolve(); + m_out_digit.resolve(); + + bool cb1 = m_output_x_cb.resolve_safe(); + bool cb2 = m_output_a_cb.resolve_safe(); + bool cb3 = m_output_digit_cb.resolve_safe(); + m_external_output = cb1 || cb2 || cb3; + + // initialize + std::fill_n(m_rowdata, ARRAY_LENGTH(m_rowdata), 0); + std::fill_n(m_rowdata_prev, ARRAY_LENGTH(m_rowdata_prev), 0); + std::fill_n(*m_bri, ARRAY_LENGTH(m_bri) * ARRAY_LENGTH(m_bri[0]), 0.0); + + m_frame_timer = machine().scheduler().timer_alloc(timer_expired_delegate(FUNC(pwm_display_device::frame_tick),this)); + m_update_time = machine().time(); + + m_rowsel = 0; + m_rowsel_prev = 0; + + // register for savestates + save_item(NAME(m_width)); + save_item(NAME(m_height)); + save_item(NAME(m_framerate_set)); + save_item(NAME(m_framerate)); + save_item(NAME(m_interpolation)); + save_item(NAME(m_levels)); + save_item(NAME(m_level_min)); + + save_item(NAME(m_segmask)); + save_item(NAME(m_rowsel)); + save_item(NAME(m_rowsel_prev)); + save_item(NAME(m_rowdata)); + save_item(NAME(m_rowdata_prev)); + + save_item(NAME(m_bri)); + save_item(NAME(m_acc)); + save_item(NAME(m_update_time)); +} + +void pwm_display_device::device_reset() +{ + if (m_height > 64 || m_width > 64) + fatalerror("%s: Invalid size %d*%d, maximum is 64*64!\n", tag(), m_height, m_width); + + schedule_frame(); + m_update_time = machine().time(); +} + + + +//------------------------------------------------- +// public handlers (most of the interface is in the .h file) +//------------------------------------------------- + +pwm_display_device &pwm_display_device::set_bri_levels(double l0, double l1, double l2, double l3) +{ + // init brightness level(s) (if you need to set more than 4, use set_bri_one) + reset_bri_levels(); + m_levels[0] = l0; + m_levels[1] = l1; + m_levels[2] = l2; + m_levels[3] = l3; + + return *this; +} + +void pwm_display_device::segmask(u64 digits, u64 mask) +{ + // set a segment mask per selected digit, but leave unselected ones alone + for (int y = 0; y < m_height; y++) + { + if (digits & 1) + m_segmask[y] = mask; + digits >>= 1; + } +} + +void pwm_display_device::matrix_partial(u8 start, u8 height, u64 rowsel, u64 rowdata, bool upd) +{ + u64 selmask = (u64(1) << height) - 1; + rowsel &= selmask; + selmask <<= start; + m_rowsel = (m_rowsel & ~selmask) | (rowsel << start); + + // update selected rows + u64 rowmask = (u64(1) << m_width) - 1; + for (int y = start; y < (start + height) && y < m_height; y++) + { + m_rowdata[y] = (rowsel & 1) ? (rowdata & rowmask) : 0; + rowsel >>= 1; + } + + if (upd) + update(); +} + +void pwm_display_device::update() +{ + // call this every time after m_rowdata is changed (automatic with matrix) + const attotime now = machine().time(); + const attotime diff = (m_update_time >= now) ? attotime::zero : now - m_update_time; + + u64 sel = m_rowsel_prev; + m_rowsel_prev = m_rowsel; + + // accumulate active time + for (int y = 0; y < m_height; y++) + { + u64 row = m_rowdata_prev[y]; + m_rowdata_prev[y] = m_rowdata[y]; + + if (diff != attotime::zero) + { + if (sel & 1) + m_acc[y][m_width] += diff; + + for (int x = 0; x < m_width; x++) + { + if (row & 1) + m_acc[y][x] += diff; + row >>= 1; + } + } + sel >>= 1; + } + + m_update_time = now; +} + + + +//------------------------------------------------- +// internal handlers +//------------------------------------------------- + +void pwm_display_device::schedule_frame() +{ + std::fill_n(*m_acc, m_height * ARRAY_LENGTH(m_acc[0]), attotime::zero); + + m_framerate = m_framerate_set; + m_frame_timer->adjust(m_framerate); +} + +TIMER_CALLBACK_MEMBER(pwm_display_device::frame_tick) +{ + update(); // final timeslice + + const double frame_time = m_framerate.as_double(); + const double factor0 = m_interpolation; + const double factor1 = 1.0 - factor0; + + for (int y = 0; y < m_height; y++) + { + u64 row = 0; + + for (int x = 0; x <= m_width; x++) + { + // determine brightness level + double bri = m_bri[y][x] * factor1 + (m_acc[y][x].as_double() / frame_time) * factor0; + if (bri > 1.0) bri = 1.0; // shouldn't happen + m_bri[y][x] = bri; + + u8 level; + for (level = 0; bri > m_levels[level]; level++) { ; } + + // output to y.x, or y.a when always-on + if (x != m_width) + { + if (level > m_level_min) + row |= (u64(1) << x); + + if (m_external_output) + m_output_x_cb(x << 6 | y, level); + else + m_out_x[y][x] = level; + } + else + { + if (m_external_output) + m_output_a_cb(y, level); + else + m_out_a[y] = level; + } + } + + // output to digity (does not support brightness levels) + if (m_segmask[y] != 0) + { + row &= m_segmask[y]; + + if (m_external_output) + m_output_digit_cb(y, row); + else + m_out_digit[y] = row; + } + } + + schedule_frame(); +} |