From 1fa00a5a3f238ea54012607c372fb0100dc762cb Mon Sep 17 00:00:00 2001 From: Florin9doi Date: Sun, 26 Feb 2023 18:42:36 +0200 Subject: [PATCH] [USB] DVD Playback Kit / MCE remote control emulation --- config_spec.yml | 139 +++++- data/dvd_remote_mask.png | Bin 0 -> 29451 bytes data/dvd_remote_mask.svg | 785 ++++++++++++++++++++++++++++++++ data/meson.build | 1 + hw/xbox/meson.build | 1 + hw/xbox/xbox_dvd_playback_kit.c | 290 ++++++++++++ hw/xbox/xid.h | 3 + ui/xemu-input.c | 94 +++- ui/xemu-input.h | 58 +++ ui/xui/gl-helpers.cc | 137 +++++- ui/xui/main-menu.cc | 39 +- 11 files changed, 1527 insertions(+), 20 deletions(-) create mode 100644 data/dvd_remote_mask.png create mode 100644 data/dvd_remote_mask.svg create mode 100644 hw/xbox/xbox_dvd_playback_kit.c diff --git a/config_spec.yml b/config_spec.yml index 5e8ad311519..efb141bb298 100644 --- a/config_spec.yml +++ b/config_spec.yml @@ -23,12 +23,16 @@ input: bindings: port1_driver: string port1: string + port1_dvd_firmware: string port2_driver: string port2: string + port2_dvd_firmware: string port3_driver: string port3: string + port3_dvd_firmware: string port4_driver: string port4: string + port4_dvd_firmware: string peripherals: port1: peripheral_type_0: integer @@ -56,7 +60,7 @@ input: default: true background_input_capture: bool keyboard_controller_scancode_map: - # Scancode reference : https://github.com/libsdl-org/SDL/blob/main/include/SDL_scancode.h + # Scancode reference : https://github.com/libsdl-org/SDL/blob/main/include/SDL3/SDL_scancode.h a: type: integer default: 4 # a @@ -132,6 +136,139 @@ input: rtrigger: type: integer default: 18 # w + keyboard_dvd_kit_scancode_map: + up: + type: integer + default: 26 # W + left: + type: integer + default: 4 # A + select: + type: integer + default: 40 # Return + right: + type: integer + default: 7 # D + down: + type: integer + default: 22 # S + display: + type: integer + default: 20 # Q + reverse: + type: integer + default: 29 # Z + play: + type: integer + default: 27 # X + forward: + type: integer + default: 25 # V + skip_down: + type: integer + default: 54 # Comma< + stop: + type: integer + default: 19 # P + pause: + type: integer + default: 6 # C + skip_up: + type: integer + default: 55 # Period> + title: + type: integer + default: 23 # T + info: + type: integer + default: 12 # I + menu: + type: integer + default: 16 # M + back: + type: integer + default: 42 # Backspace + button1: + type: integer + default: 30 # 1 + button2: + type: integer + default: 31 # 2 + button3: + type: integer + default: 32 # 3 + button4: + type: integer + default: 33 # 4 + button5: + type: integer + default: 34 # 5 + button6: + type: integer + default: 35 # 6 + button7: + type: integer + default: 36 # 7 + button8: + type: integer + default: 37 # 8 + button9: + type: integer + default: 38 # 9 + button0: + type: integer + default: 39 # 0 + power: + type: integer + default: 58 # F1 + my_tv: + type: integer + default: 59 # F2 + my_music: + type: integer + default: 60 # F3 + my_pictures: + type: integer + default: 61 # F4 + my_videos: + type: integer + default: 62 # F5 + record: + type: integer + default: 63 # F6 + start: + type: integer + default: 64 # F7 + volume_up: + type: integer + default: 65 # F8 + volume_down: + type: integer + default: 66 # F9 + mute: + type: integer + default: 67 # F10 + channel_up: + type: integer + default: 68 # F11 + channel_down: + type: integer + default: 69 # F12 + recorded_tv: + type: integer + default: 21 # R + live_tv: + type: integer + default: 15 # L + star: + type: integer + default: 45 # minus- + pound: + type: integer + default: 46 # equal= + clear: + type: integer + default: 49 # backslash\ keyboard_sbc_scancode_map: eject: type: integer diff --git a/data/dvd_remote_mask.png b/data/dvd_remote_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..eba245385ef9ab7a773615f7c313942fca97bd03 GIT binary patch literal 29451 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

$1GKvn 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 GJpK + + + + + + + DVD + DISPLAY + + REVERSE + PLAY + FORWARD + + + + SKIP- + STOP + PAUSE + SKIP+ + + + + + TITLE + INFO + + + + + SELECT + + + + + + MENU + BACK + + + + + + + + + + + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + X + XBOX + + + + + + + + + + + My TV + My Music + My Pictures + My Videos + + + + STOP + REC + PAUSE + + + REW + FWD + + + + REPLAY + + SKIP + + + BACK + MORE + + + + + + OK + START + + + + + + + VOL + MUTE + CH/PG + + + + + - + - + RECORDED + TV + GUIDE + LIVE TV + DVD + MENU + + + + + + + + + + + + + + + + + ABC + DEF + 1 + 2 + 3 + GHI + JKL + MNO + 4 + 5 + 6 + PQRS + TUV + WXYZ + 7 + 8 + 9 + + * + 0 + # + CLEAR + ENTER + + + XBOX + + + diff --git a/data/meson.build b/data/meson.build index 87ea47e260b..859fcfd8b42 100644 --- a/data/meson.build +++ b/data/meson.build @@ -2,6 +2,7 @@ pfiles = [ 'sb_controller_mask.png', 'controller_mask.png', 'controller_mask_s.png', + 'dvd_remote_mask.png', 'xmu_mask.png', 'logo_sdf.png', 'xemu_64x64.png', diff --git a/hw/xbox/meson.build b/hw/xbox/meson.build index b3b18e4c64f..25dbf4d5aef 100644 --- a/hw/xbox/meson.build +++ b/hw/xbox/meson.build @@ -13,6 +13,7 @@ specific_ss.add(files( 'smbus_storage.c', 'smbus_xbox_smc.c', 'xbox.c', + 'xbox_dvd_playback_kit.c', 'xbox_pci.c', 'xid.c', 'xblc.c', diff --git a/hw/xbox/xbox_dvd_playback_kit.c b/hw/xbox/xbox_dvd_playback_kit.c new file mode 100644 index 00000000000..f5f321f1723 --- /dev/null +++ b/hw/xbox/xbox_dvd_playback_kit.c @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2025 Florin9doi + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include "xid.h" + +typedef struct XboxDVDPlaybackKitReport { + uint8_t bReportId; + uint8_t bLength; + uint16_t wButton; + uint16_t wTimer; +} QEMU_PACKED XboxDVDPlaybackKitReport; + +typedef struct XboxDVDPlaybackKitState { + USBDevice dev; + uint8_t device_index; + char *firmware_path; + uint32_t firmware_len; + uint8_t firmware[0x40000]; + XboxDVDPlaybackKitReport in_state; +} XboxDVDPlaybackKitState; + +enum { + STR_EMPTY +}; + +static const USBDescIface desc_iface[] = { + { + .bInterfaceNumber = 0, + .bAlternateSetting = 0, + .bNumEndpoints = 1, + .bInterfaceClass = 0x58, // USB_CLASS_XID, + .bInterfaceSubClass = 0x42, // USB_DT_XID + .bInterfaceProtocol = 0, + .iInterface = STR_EMPTY, + .eps = (USBDescEndpoint[]) { + { + .bEndpointAddress = USB_DIR_IN | 0x01, + .bmAttributes = USB_ENDPOINT_XFER_INT, + .wMaxPacketSize = 8, + .bInterval = 16, + }, + }, + }, + { + .bInterfaceNumber = 1, + .bAlternateSetting = 0, + .bNumEndpoints = 0, + .bInterfaceClass = 0x59, + .bInterfaceSubClass = 0, + .bInterfaceProtocol = 0, + .iInterface = STR_EMPTY, + }, +}; + +static const USBDescDevice desc_device = { + .bcdUSB = 0x0110, + .bDeviceClass = 0, + .bDeviceSubClass = 0, + .bDeviceProtocol = 0, + .bMaxPacketSize0 = 64, + .bNumConfigurations = 1, + .confs = (USBDescConfig[]) { + { + .bNumInterfaces = 2, + .bConfigurationValue = 1, + .iConfiguration = STR_EMPTY, + .bmAttributes = 0x00, + .bMaxPower = 0x00, + .nif = ARRAY_SIZE(desc_iface), + .ifs = desc_iface, + }, + }, +}; + +static const USBDesc desc_xbox_dvd_playback_kit = { + .id = { + .idVendor = 0x045e, + .idProduct = 0x0284, + .bcdDevice = 0x0100, + .iManufacturer = STR_EMPTY, + .iProduct = STR_EMPTY, + .iSerialNumber = STR_EMPTY, + }, + .full = &desc_device, +}; + +static const XIDDesc desc_xid_xbox_dvd_playback_kit = { + .bLength = 0x08, + .bDescriptorType = USB_DT_XID, + .bcdXid = 0x0100, + .bType = XID_DEVICETYPE_DVD_PLAYBACK_KIT, + .bSubType = XID_DEVICESUBTYPE_DVD_PLAYBACK_KIT, + .bMaxInputReportSize = 0x06, + .bMaxOutputReportSize = 0x00, +}; + +struct { + uint64_t btn; + uint16_t id; +} button_ids[] = { + {DVD_BUTTON_UP, 0x0AA6}, + {DVD_BUTTON_LEFT, 0x0AA9}, + {DVD_BUTTON_SELECT, 0x0A0B}, + {DVD_BUTTON_RIGHT, 0x0AA8}, + {DVD_BUTTON_DOWN, 0x0AA7}, + {DVD_BUTTON_DISPLAY, 0x0AD5}, + {DVD_BUTTON_REVERSE, 0x0AE2}, + {DVD_BUTTON_PLAY, 0x0AEA}, + {DVD_BUTTON_FORWARD, 0x0AE3}, + {DVD_BUTTON_SKIP_DOWN, 0x0ADD}, + {DVD_BUTTON_STOP, 0x0AE0}, + {DVD_BUTTON_PAUSE, 0x0AE6}, + {DVD_BUTTON_SKIP_UP, 0x0ADF}, + {DVD_BUTTON_TITLE, 0x0AE5}, + {DVD_BUTTON_INFO, 0x0AC3}, + {DVD_BUTTON_MENU, 0x0AF7}, + {DVD_BUTTON_BACK, 0x0AD8}, + {DVD_BUTTON_1, 0x0ACE}, + {DVD_BUTTON_2, 0x0ACD}, + {DVD_BUTTON_3, 0x0ACC}, + {DVD_BUTTON_4, 0x0ACB}, + {DVD_BUTTON_5, 0x0ACA}, + {DVD_BUTTON_6, 0x0AC9}, + {DVD_BUTTON_7, 0x0AC8}, + {DVD_BUTTON_8, 0x0AC7}, + {DVD_BUTTON_9, 0x0AC6}, + {DVD_BUTTON_0, 0x0ACF}, + // Media Center Extender Remote + {MCE_BUTTON_POWER, 0x0AC4}, + {MCE_BUTTON_MY_TV, 0x0A31}, + {MCE_BUTTON_MY_MUSIC, 0x0A09}, + {MCE_BUTTON_MY_PICTURES, 0x0A06}, + {MCE_BUTTON_MY_VIDEOS, 0x0A07}, + {MCE_BUTTON_RECORD, 0x0AE8}, + {MCE_BUTTON_START, 0x0A25}, + {MCE_BUTTON_VOL_UP, 0x0AD0}, + {MCE_BUTTON_VOL_DOWN, 0x0AD1}, + {MCE_BUTTON_MUTE, 0x0AC0}, + {MCE_BUTTON_CH_UP, 0x0AD2}, + {MCE_BUTTON_CH_DOWN, 0x0AD3}, + {MCE_BUTTON_RECORDED_TV, 0x0A65}, + // {GUIDE, }, ??? + {MCE_BUTTON_LIVE_TV, 0x0A18}, + {MCE_BUTTON_STAR, 0x0A28}, + {MCE_BUTTON_POUND, 0x0A29}, + {MCE_BUTTON_CLEAR, 0x0AF9}, +}; + +static void xbox_dvd_playback_kit_realize(USBDevice *dev, Error **errp) { + XboxDVDPlaybackKitState *s = (XboxDVDPlaybackKitState *) dev; + + usb_desc_init(dev); + if (!s->firmware_path) { + fprintf(stderr, "Firmware file is required"); + s->firmware_len = 0; + return; + } + int fd = open(s->firmware_path, O_RDONLY | O_BINARY); + if (fd < 0) { + fprintf(stderr, "Unable to access \"%s\"", s->firmware_path); + s->firmware_len = 0; + return; + } + size_t size = lseek(fd, 0, SEEK_END); + lseek(fd, 0, SEEK_SET); + s->firmware_len = read(fd, s->firmware, size); + close(fd); +} + +static void xbox_dvd_playback_kit_handle_control(USBDevice *dev, USBPacket *p, + int request, int value, int index, int length, uint8_t *data) { + XboxDVDPlaybackKitState *s = (XboxDVDPlaybackKitState *) dev; + + int ret = usb_desc_handle_control(dev, p, request, value, index, length, data); + if (ret >= 0) { + return; + } + + switch (request) { + case 0xc101: + case 0xc102: + { + uint32_t offset = 0x400 * value; + if (offset + length <= s->firmware_len) { + memcpy(data, s->firmware + offset, length); + p->actual_length = length; + } else { + p->actual_length = 0; + } + break; + } + case 0xc106: // GET_DESCRIPTOR + memcpy(data, &desc_xid_xbox_dvd_playback_kit, desc_xid_xbox_dvd_playback_kit.bLength); + p->actual_length = desc_xid_xbox_dvd_playback_kit.bLength; + break; + case 0xa101: // GET_REPORT + default: + p->actual_length = 0; + p->status = USB_RET_STALL; + break; + } +} + +static void update_dvd_kit_input(XboxDVDPlaybackKitState *s) +{ + if (xemu_input_get_test_mode()) { + // Don't report changes if we are testing the controller while running + return; + } + + ControllerState *state = xemu_input_get_bound(s->device_index); + assert(state); + xemu_input_update_controller(state); + + s->in_state.bReportId = 0x00; + s->in_state.bLength = 0x06; + if (!state->dvdKit.buttons) { + s->in_state.wButton = 0x0000; + s->in_state.wTimer = 0xffff; + return; + } + for (int i = 0; i < sizeof(button_ids) / sizeof(button_ids[0]); i++) { + if ((1ULL << i) & state->dvdKit.buttons) { + s->in_state.wButton = button_ids[i].id; + s->in_state.wTimer = 0x0040; + return; + } + } +} + +static void xbox_dvd_playback_kit_handle_data(USBDevice *dev, USBPacket *p) { + XboxDVDPlaybackKitState *s = DO_UPCAST(XboxDVDPlaybackKitState, dev, dev); + + switch (p->pid) { + case USB_TOKEN_IN: + update_dvd_kit_input(s); + usb_packet_copy(p, &s->in_state, s->in_state.bLength); + break; + case USB_TOKEN_OUT: + default: + break; + } +} + +static Property xid_properties[] = { + DEFINE_PROP_UINT8("index", XboxDVDPlaybackKitState, device_index, 0), + DEFINE_PROP_STRING("firmware", XboxDVDPlaybackKitState, firmware_path), + DEFINE_PROP_END_OF_LIST(), +}; + +static void xbox_dvd_playback_kit_class_init(ObjectClass *klass, void *class_data) { + DeviceClass *dc = DEVICE_CLASS(klass); + USBDeviceClass *uc = USB_DEVICE_CLASS(klass); + + uc->product_desc = "Microsoft Xbox DVD Playback Kit"; + uc->usb_desc = &desc_xbox_dvd_playback_kit; + uc->realize = xbox_dvd_playback_kit_realize; + uc->handle_control = xbox_dvd_playback_kit_handle_control; + uc->handle_data = xbox_dvd_playback_kit_handle_data; + + device_class_set_props(dc, xid_properties); + dc->desc = "Microsoft Xbox DVD Playback Kit"; +} + +static const TypeInfo xbox_dvd_playback_kit_info = { + .name = TYPE_USB_XBOX_DVD_PLAYBACK_KIT, + .parent = TYPE_USB_DEVICE, + .instance_size = sizeof(XboxDVDPlaybackKitState), + .class_init = xbox_dvd_playback_kit_class_init, +}; + +static void usb_xbox_dvd_playback_kit_register_types(void) { + type_register_static(&xbox_dvd_playback_kit_info); +} + +type_init(usb_xbox_dvd_playback_kit_register_types) diff --git a/hw/xbox/xid.h b/hw/xbox/xid.h index 5acbed0d824..2ca9d4877e7 100644 --- a/hw/xbox/xid.h +++ b/hw/xbox/xid.h @@ -48,14 +48,17 @@ #define XID_GET_CAPABILITIES 0x01 #define XID_DEVICETYPE_GAMEPAD 0x01 +#define XID_DEVICETYPE_DVD_PLAYBACK_KIT 0x03 #define XID_DEVICETYPE_STEEL_BATTALION 0x80 +#define XID_DEVICESUBTYPE_DVD_PLAYBACK_KIT 0x00 #define XID_DEVICESUBTYPE_GAMEPAD 0x01 #define XID_DEVICESUBTYPE_GAMEPAD_S 0x02 #define TYPE_USB_XID_GAMEPAD "usb-xbox-gamepad" #define TYPE_USB_XID_GAMEPAD_S "usb-xbox-gamepad-s" #define TYPE_USB_XID_STEEL_BATTALION "usb-steel-battalion" +#define TYPE_USB_XBOX_DVD_PLAYBACK_KIT "xbox-dvd-playback-kit" #define GAMEPAD_A 0 #define GAMEPAD_B 1 diff --git a/ui/xemu-input.c b/ui/xemu-input.c index e20bfcfcf88..86f0c9b9aba 100644 --- a/ui/xemu-input.c +++ b/ui/xemu-input.c @@ -104,10 +104,17 @@ static const char **port_index_to_settings_key_map[] = { static const char **port_index_to_driver_settings_key_map[] = { &g_config.input.bindings.port1_driver, &g_config.input.bindings.port2_driver, - &g_config.input.bindings.port3_driver, + &g_config.input.bindings.port3_driver, &g_config.input.bindings.port4_driver }; +static const char **port_index_to_dvd_firmware_key_map[] = { + &g_config.input.bindings.port1_dvd_firmware, + &g_config.input.bindings.port2_dvd_firmware, + &g_config.input.bindings.port3_dvd_firmware, + &g_config.input.bindings.port4_dvd_firmware, +}; + static int *peripheral_types_settings_map[4][2] = { { &g_config.input.peripherals.port1.peripheral_type_0, &g_config.input.peripherals.port1.peripheral_type_1 }, @@ -132,6 +139,7 @@ static const char **peripheral_params_settings_map[4][2] = { static int sdl_kbd_scancode_map[25]; static int sdl_sbc_kbd_scancode_map[52]; +static int sdl_dvd_kit_kbd_scancode_map[44]; static const char *get_bound_driver(int port) { @@ -150,6 +158,8 @@ static const char *get_bound_driver(int port) return DRIVER_S; if (strcmp(driver, DRIVER_STEEL_BATTALION) == 0) return DRIVER_STEEL_BATTALION; + if (strcmp(driver, DEVICE_DVD_PLAYBACK_KIT) == 0) + return DEVICE_DVD_PLAYBACK_KIT; return DRIVER_DUKE; } @@ -321,6 +331,54 @@ void xemu_input_init(void) } } + { + int i = 0; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.up; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.left; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.select; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.right; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.down; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.display; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.reverse; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.play; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.forward; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.skip_down; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.stop; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.pause; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.skip_up; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.title; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.info; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.menu; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.back; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button1; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button2; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button3; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button4; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button5; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button6; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button7; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button8; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button9; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.button0; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.power; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.my_tv; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.my_music; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.my_pictures; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.my_videos; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.record; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.start; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.volume_up; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.volume_down; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.mute; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.channel_up; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.channel_down; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.recorded_tv; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.live_tv; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.star; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.pound; + sdl_dvd_kit_kbd_scancode_map[i++] = g_config.input.keyboard_dvd_kit_scancode_map.clear; + } + bound_drivers[0] = get_bound_driver(0); bound_drivers[1] = get_bound_driver(1); bound_drivers[2] = get_bound_driver(2); @@ -375,6 +433,14 @@ void xemu_save_peripheral_settings(int player_index, int peripheral_index, peripheral_parameter == NULL ? "" : peripheral_parameter); } +void xemu_save_dvd_firmware_settings(int player_index, const char *firmware) +{ + ControllerState *state = bound_controllers[player_index]; + state->dvdKit.firmware = g_strdup(firmware); + xemu_settings_set_string(port_index_to_dvd_firmware_key_map[player_index], + firmware); +} + void xemu_input_process_sdl_events(const SDL_Event *event) { if (event->type == SDL_CONTROLLERDEVICEADDED) { @@ -403,6 +469,7 @@ void xemu_input_process_sdl_events(const SDL_Event *event) new_con->peripheral_types[1] = PERIPHERAL_NONE; new_con->peripherals[0] = NULL; new_con->peripherals[1] = NULL; + new_con->dvdKit.firmware = NULL; char guid_buf[35] = { 0 }; SDL_JoystickGetGUIDString(new_con->sdl_joystick_guid, guid_buf, sizeof(guid_buf)); @@ -540,6 +607,7 @@ void xemu_input_update_sdl_kbd_controller_state(ControllerState *state) { state->gp.buttons = 0; state->sbc.buttons = 0; + state->dvdKit.buttons = 0; memset(state->gp.axis, 0, sizeof(state->gp.axis)); memset(state->sbc.axis, 0, sizeof(state->sbc.axis)); @@ -690,6 +758,10 @@ void xemu_input_update_sdl_kbd_controller_state(ControllerState *state) state->sbc.axis[SBC_AXIS_MIDDLE_PEDAL] = 32767; state->sbc.previousButtons = state->sbc.buttons; + } else if (strcmp(bound_driver, DEVICE_DVD_PLAYBACK_KIT) == 0) { + for (int i = 0; i < 44; i++) { + state->dvdKit.buttons |= (unsigned long long)kbd[sdl_dvd_kit_kbd_scancode_map[i]] << i; + } } else { // Update Gamepad Buttons for (int i = 0; i < 15; i++) { @@ -952,7 +1024,9 @@ void xemu_input_bind(int index, ControllerState *state, int save) QDict *usbhub_qdict = NULL; DeviceState *usbhub_dev = NULL; - bool hasInternalHub = strcmp(bound_drivers[index], DRIVER_STEEL_BATTALION) != 0; + bool hasInternalHub = strcmp(bound_drivers[index], DRIVER_STEEL_BATTALION) != 0 + && strcmp(bound_drivers[index], DEVICE_DVD_PLAYBACK_KIT) != 0; + bool hasFirmware = strcmp(bound_drivers[index], DEVICE_DVD_PLAYBACK_KIT) == 0; if (hasInternalHub) { // Create controller's internal USB hub. @@ -985,6 +1059,12 @@ void xemu_input_bind(int index, ControllerState *state, int save) qdict_put_str(qdict, "port", tmp); g_free(tmp); + // Specify DVD firmware + if (hasFirmware) { + qdict_put_str(qdict, "firmware", *port_index_to_dvd_firmware_key_map[index]); + state->dvdKit.firmware = (char*) *port_index_to_dvd_firmware_key_map[index]; + } + // Create the device QemuOpts *opts = qemu_opts_from_qdict(qemu_find_opts("device"), qdict, &error_abort); DeviceState *dev = qdev_device_add(opts, &error_abort); @@ -1008,8 +1088,8 @@ bool xemu_input_bind_xmu(int player_index, int expansion_slot_index, assert(player_index >= 0 && player_index < 4); assert(expansion_slot_index >= 0 && expansion_slot_index < 2); - bool hasInternalHub = - strcmp(bound_drivers[player_index], DRIVER_STEEL_BATTALION) != 0; + bool hasInternalHub = strcmp(bound_drivers[player_index], DRIVER_STEEL_BATTALION) != 0 + && strcmp(bound_drivers[player_index], DEVICE_DVD_PLAYBACK_KIT) != 0; assert(hasInternalHub); ControllerState *player = bound_controllers[player_index]; @@ -1134,11 +1214,11 @@ void xemu_input_unbind_xmu(int player_index, int expansion_slot_index) void xemu_input_rebind_xmu(int port) { - bool hasInternalHub = - strcmp(bound_drivers[port], DRIVER_STEEL_BATTALION) != 0; + bool hasInternalHub = strcmp(bound_drivers[port], DRIVER_STEEL_BATTALION) != 0 + && strcmp(bound_drivers[port], DEVICE_DVD_PLAYBACK_KIT) != 0; if (!hasInternalHub) return; - + // Try to bind peripherals back to controller for (int i = 0; i < 2; i++) { enum peripheral_type peripheral_type = diff --git a/ui/xemu-input.h b/ui/xemu-input.h index 3fecc7ec29d..1d68082a133 100644 --- a/ui/xemu-input.h +++ b/ui/xemu-input.h @@ -33,10 +33,12 @@ #define DRIVER_DUKE "usb-xbox-gamepad" #define DRIVER_S "usb-xbox-gamepad-s" #define DRIVER_STEEL_BATTALION "usb-steel-battalion" +#define DEVICE_DVD_PLAYBACK_KIT "xbox-dvd-playback-kit" #define DRIVER_DUKE_DISPLAY_NAME "Xbox Controller" #define DRIVER_S_DISPLAY_NAME "Xbox Controller S" #define DRIVER_STEEL_BATTALION_DISPLAY_NAME "Steel Battalion Controller" +#define DEVICE_DVD_PLAYBACK_KIT_DISPLAY_NAME "Xbox DVD Playback Kit" enum controller_state_buttons_mask { CONTROLLER_BUTTON_A = (1 << 0), @@ -106,6 +108,55 @@ enum steel_battalion_controller_state_buttons_mask { #define SBC_BUTTON_TUNER_LEFT 0x20000000000ULL #define SBC_BUTTON_TUNER_RIGHT 0x40000000000ULL +enum dvd_playback_kit_state_buttons_mask { + DVD_BUTTON_UP = (1 << 0), + DVD_BUTTON_LEFT = (1 << 1), + DVD_BUTTON_SELECT = (1 << 2), + DVD_BUTTON_RIGHT = (1 << 3), + DVD_BUTTON_DOWN = (1 << 4), + DVD_BUTTON_DISPLAY = (1 << 5), + DVD_BUTTON_REVERSE = (1 << 6), + DVD_BUTTON_PLAY = (1 << 7), + DVD_BUTTON_FORWARD = (1 << 8), + DVD_BUTTON_SKIP_DOWN = (1 << 9), + DVD_BUTTON_STOP = (1 << 10), + DVD_BUTTON_PAUSE = (1 << 11), + DVD_BUTTON_SKIP_UP = (1 << 12), + DVD_BUTTON_TITLE = (1 << 13), + DVD_BUTTON_INFO = (1 << 14), + DVD_BUTTON_MENU = (1 << 15), + DVD_BUTTON_BACK = (1 << 16), + DVD_BUTTON_1 = (1 << 17), + DVD_BUTTON_2 = (1 << 18), + DVD_BUTTON_3 = (1 << 19), + DVD_BUTTON_4 = (1 << 20), + DVD_BUTTON_5 = (1 << 21), + DVD_BUTTON_6 = (1 << 22), + DVD_BUTTON_7 = (1 << 23), + DVD_BUTTON_8 = (1 << 24), + DVD_BUTTON_9 = (1 << 25), + DVD_BUTTON_0 = (1 << 26), + // Media Center Extender Remote + MCE_BUTTON_POWER = (1 << 27), + MCE_BUTTON_MY_TV = (1 << 28), + MCE_BUTTON_MY_MUSIC = (1 << 29), + MCE_BUTTON_MY_PICTURES = (1 << 30), + MCE_BUTTON_MY_VIDEOS = (1 << 31), + MCE_BUTTON_RECORD = (1ULL << 32), + MCE_BUTTON_START = (1ULL << 33), + MCE_BUTTON_VOL_UP = (1ULL << 34), + MCE_BUTTON_VOL_DOWN = (1ULL << 35), + MCE_BUTTON_MUTE = (1ULL << 36), + MCE_BUTTON_CH_UP = (1ULL << 37), + MCE_BUTTON_CH_DOWN = (1ULL << 38), + MCE_BUTTON_RECORDED_TV = (1ULL << 39), + // GUIDE? + MCE_BUTTON_LIVE_TV = (1ULL << 40), + MCE_BUTTON_STAR = (1ULL << 41), + MCE_BUTTON_POUND = (1ULL << 42), + MCE_BUTTON_CLEAR = (1ULL << 43), +}; + enum controller_state_axis_index { CONTROLLER_AXIS_LTRIG, CONTROLLER_AXIS_RTRIG, @@ -164,6 +215,11 @@ typedef struct SteelBattalionState { uint8_t toggleSwitches; } SteelBattalionState; +typedef struct { + char *firmware; + uint64_t buttons; +} DVDPlaybackKitState; + typedef struct ControllerState { QTAILQ_ENTRY(ControllerState) entry; @@ -172,6 +228,7 @@ typedef struct ControllerState { GamepadState gp; SteelBattalionState sbc; + DVDPlaybackKitState dvdKit; enum controller_input_device_type type; const char *name; @@ -213,6 +270,7 @@ int xemu_input_get_controller_default_bind_port(ControllerState *state, int star void xemu_save_peripheral_settings(int player_index, int peripheral_index, int peripheral_type, const char *peripheral_parameter); +void xemu_save_dvd_firmware_settings(int player_index, const char *firmware); void xemu_input_set_test_mode(int enabled); int xemu_input_get_test_mode(void); diff --git a/ui/xui/gl-helpers.cc b/ui/xui/gl-helpers.cc index 10a9524ba78..48d18a8be1b 100644 --- a/ui/xui/gl-helpers.cc +++ b/ui/xui/gl-helpers.cc @@ -21,6 +21,7 @@ #include "common.hh" #include "data/controller_mask.png.h" #include "data/controller_mask_s.png.h" +#include "data/dvd_remote_mask.png.h" #include "data/sb_controller_mask.png.h" #include "data/logo_sdf.png.h" #include "data/xemu_64x64.png.h" @@ -37,7 +38,7 @@ extern int viewport_coords[4]; Fbo *controller_fbo, *xmu_fbo, *logo_fbo; -GLuint g_controller_duke_tex, g_controller_s_tex, g_sb_controller_tex, g_logo_tex, g_icon_tex, g_xmu_tex; +GLuint g_controller_duke_tex, g_controller_s_tex, g_sb_controller_tex, g_dvd_remote_tex, g_logo_tex, g_icon_tex, g_xmu_tex; enum class ShaderType { Blit, @@ -474,6 +475,8 @@ void InitCustomRendering(void) LoadTextureFromMemory(controller_mask_s_data, controller_mask_s_size); g_sb_controller_tex = LoadTextureFromMemory(sb_controller_mask_data, sb_controller_mask_size); + g_dvd_remote_tex = + LoadTextureFromMemory(dvd_remote_mask_data, dvd_remote_mask_size); g_decal_shader = NewDecalShader(ShaderType::Mask); controller_fbo = new Fbo(512, 512); @@ -1115,6 +1118,136 @@ void RenderSteelBattalionController(float frame_x, float frame_y, uint32_t prima glUseProgram(0); } +void RenderDVDRemote(float frame_x, float frame_y, uint32_t primary_color, + uint32_t secondary_color, ControllerState *state) +{ + // Location within the controller texture of masked button locations, + // relative to the origin of the controller + const struct rect dvd_buttons[] = { + { 108, 248, 20, 10 }, // UP + { 78, 226, 20, 10 }, // LEFT + { 106, 225, 24, 11 }, // SELECT + { 139, 226, 19, 11 }, // RIGHT + { 109, 203, 19, 11 }, // DOWN + { 106, 326, 24, 11 }, // DISPLAY + { 74, 300, 24, 11 }, // REVERSE + { 106, 300, 24, 11 }, // PLAY + { 139, 300, 24, 11 }, // FORWARD + { 74, 274, 11, 11 }, // SKIP- + { 100, 274, 11, 11 }, // STOP + { 126, 274, 11, 11 }, // PAUSE + { 152, 274, 11, 11 }, // SKIP+ + { 74, 248, 11, 11 }, // TITLE + { 152, 248, 11, 11 }, // INFO + { 74, 202, 11, 12 }, // MENU + { 152, 202, 11, 12 }, // BACK + { 80, 176, 11, 12 }, // 1 + { 113, 176, 11, 12 }, // 2 + { 145, 176, 11, 12 }, // 3 + { 80, 154, 11, 11 }, // 4 + { 113, 154, 11, 11 }, // 5 + { 145, 154, 11, 11 }, // 6 + { 80, 131, 11, 11 }, // 7 + { 113, 131, 11, 11 }, // 8 + { 145, 131, 11, 11 }, // 9 + { 113, 108, 11, 11 }, // 0 + }; + const struct rect mce_buttons[] = { + { 339, 267, 16, 8 }, // UP + { 312, 251, 16, 8 }, // LEFT + { 337, 251, 21, 8 }, // SELECT/OK + { 366, 251, 16, 8 }, // RIGHT + { 339, 235, 16, 8 }, // DOWN + { 340, 40, 14, 8 }, // DISPLAY/XBOX + { 298, 306, 8, 8 }, // REVERSE/REW + { 340, 290, 14,15 }, // PLAY + { 389, 306, 8, 8 }, // FORWARD/FWD + { 320, 290, 8, 8 }, // SKIP-/REPLAY + { 343, 313, 8, 8 }, // STOP + { 366, 306, 8, 8 }, // PAUSE + { 366, 290, 8, 8 }, // SKIP+/SKIP + { 0, 0, 0, 0 }, // TITLE/GUIDE? + { 385, 271, 8, 8 }, // INFO/MORE + { 382, 176, 11, 8 }, // MENU/DVD MENU + { 301, 271, 8, 8 }, // BACK + { 307, 154, 15, 8 }, // 1 + { 340, 154, 15, 8 }, // 2 + { 372, 154, 15, 8 }, // 3 + { 307, 134, 15, 8 }, // 4 + { 340, 134, 15, 8 }, // 5 + { 372, 134, 15, 8 }, // 6 + { 307, 115, 15, 8 }, // 7 + { 340, 115, 15, 8 }, // 8 + { 372, 115, 15, 8 }, // 9 + { 340, 95, 15, 8 }, // 0 + { 395, 342, 8, 8 }, // POWER + { 301, 323, 8, 8 }, // MY_TV + { 327, 332, 8, 8 }, // MY_MUSIC + { 359, 332, 8, 8 }, // MY_PICTURES + { 385, 323, 8, 8 }, // MY_VIDEOS + { 320, 306, 8, 8 }, // RECORD + { 337, 215, 21, 8 }, // START + { 298, 225, 21, 8 }, // VOL_UP + { 304, 206, 21, 8 }, // VOL_DOWN + { 337, 196, 21, 8 }, // MUTE + { 376, 225, 21, 8 }, // CH_UP + { 369, 206, 21, 8 }, // CH_DOWN + { 301, 176, 11, 8 }, // RECORDED_TV + // GUIDE? + { 356, 173, 11, 8 }, // LIVE_TV + { 307, 95, 15, 8 }, // STAR + { 372, 95, 15, 8 }, // POUND + { 327, 72, 11, 8 }, // CLEAR + }; + const struct rect mce_enter_button = + { 356, 72, 11, 8 }; // ENTER + + glUseProgram(g_decal_shader->prog); + glBindVertexArray(g_decal_shader->vao); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, g_dvd_remote_tex); + + glBlendEquation(GL_FUNC_ADD); + glBlendFunc(GL_ONE, GL_ZERO); + + // Render controller texture + RenderDecal(g_decal_shader, frame_x + 0, frame_y + 0, + tex_items[obj_controller].w, tex_items[obj_controller].h, + tex_items[obj_controller].x, tex_items[obj_controller].y, + tex_items[obj_controller].w, tex_items[obj_controller].h, + primary_color, secondary_color, 0); + + glBlendFunc(GL_ONE_MINUS_DST_ALPHA, + GL_ONE); // Blend with controller cutouts + + // The controller has alpha cutouts where the buttons are. Draw a surface + // behind the buttons if they are activated + for (int i = 0; i < sizeof(dvd_buttons) / sizeof(dvd_buttons[0]); i++) { + if (state->dvdKit.buttons & (1ULL << i)) { + RenderDecal(g_decal_shader, frame_x + dvd_buttons[i].x, + frame_y + dvd_buttons[i].y, dvd_buttons[i].w, dvd_buttons[i].h, 0, + 0, 1, 1, 0, 0, primary_color + 0xff); + } + } + for (int i = 0; i < sizeof(mce_buttons) / sizeof(mce_buttons[0]); i++) { + if (state->dvdKit.buttons & (1ULL << i)) { + RenderDecal(g_decal_shader, frame_x + mce_buttons[i].x, + frame_y + mce_buttons[i].y, mce_buttons[i].w, mce_buttons[i].h, 0, + 0, 1, 1, 0, 0, primary_color + 0xff); + } + } + if (state->dvdKit.buttons & DVD_BUTTON_SELECT) { + RenderDecal(g_decal_shader, frame_x + mce_enter_button.x, + frame_y + mce_enter_button.y, mce_enter_button.w, mce_enter_button.h, 0, + 0, 1, 1, 0, 0, primary_color + 0xff); + } + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blend with controller + + glBindVertexArray(0); + glUseProgram(0); +} + void RenderController(float frame_x, float frame_y, uint32_t primary_color, uint32_t secondary_color, ControllerState *state) { @@ -1127,6 +1260,8 @@ void RenderController(float frame_x, float frame_y, uint32_t primary_color, else if (strcmp(bound_drivers[state->bound], DRIVER_DUKE) == 0) RenderDukeController(frame_x, frame_y, primary_color, secondary_color, state); + else if (strcmp(bound_drivers[state->bound], DEVICE_DVD_PLAYBACK_KIT) == 0) + RenderDVDRemote(frame_x, frame_y, primary_color, secondary_color, state); } void RenderControllerPort(float frame_x, float frame_y, int i, diff --git a/ui/xui/main-menu.cc b/ui/xui/main-menu.cc index ab1fd7fe6a8..f75738ce621 100644 --- a/ui/xui/main-menu.cc +++ b/ui/xui/main-menu.cc @@ -171,20 +171,23 @@ void MainMenuInputView::Draw() driver = DRIVER_S_DISPLAY_NAME; else if (strcmp(driver, DRIVER_STEEL_BATTALION) == 0) driver = DRIVER_STEEL_BATTALION_DISPLAY_NAME; - + else if (strcmp(driver, DEVICE_DVD_PLAYBACK_KIT) == 0) + driver = DEVICE_DVD_PLAYBACK_KIT_DISPLAY_NAME; + ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::BeginCombo("###InputDrivers", driver, ImGuiComboFlags_NoArrowButton)) { - const char *available_drivers[] = { - DRIVER_DUKE, + const char *available_drivers[] = { + DRIVER_DUKE, DRIVER_S, - DRIVER_STEEL_BATTALION + DRIVER_STEEL_BATTALION, + DEVICE_DVD_PLAYBACK_KIT }; - const char *driver_display_names[] = { - DRIVER_DUKE_DISPLAY_NAME, - DRIVER_S_DISPLAY_NAME, - DRIVER_STEEL_BATTALION_DISPLAY_NAME - + const char *driver_display_names[] = { + DRIVER_DUKE_DISPLAY_NAME, + DRIVER_S_DISPLAY_NAME, + DRIVER_STEEL_BATTALION_DISPLAY_NAME, + DEVICE_DVD_PLAYBACK_KIT_DISPLAY_NAME }; bool is_selected = false; int num_drivers = sizeof(driver_display_names) / sizeof(driver_display_names[0]); @@ -329,8 +332,9 @@ void MainMenuInputView::Draw() ImGui::SetCursorPos(pos); if (bound_state) { - bool hasInternalHub = - strcmp(bound_drivers[active], DRIVER_STEEL_BATTALION) != 0; + bool hasInternalHub = strcmp(bound_drivers[active], DRIVER_STEEL_BATTALION) != 0 + && strcmp(bound_drivers[active], DEVICE_DVD_PLAYBACK_KIT) != 0; + bool hasFirmware = strcmp(bound_drivers[active], DEVICE_DVD_PLAYBACK_KIT) == 0; if (hasInternalHub) { SectionTitle("Expansion Slots"); // Begin a 2-column layout to render the expansion slots @@ -492,6 +496,19 @@ void MainMenuInputView::Draw() ImGui::PopStyleVar(); // ItemSpacing ImGui::Columns(1); } + if (hasFirmware) { + SectionTitle("Firmware"); + const char *firmware_filters = ".bin Files\0*.bin\0All Files\0*.*\0"; + const char *firmware_path = NULL; + if (bound_state->dvdKit.firmware == NULL) + firmware_path = g_strdup(""); + else + firmware_path = g_strdup(bound_state->dvdKit.firmware); + if (FilePicker("DVD Kit Firmware", &firmware_path, firmware_filters)) { + xemu_save_dvd_firmware_settings(active, firmware_path); + } + g_free((void *)firmware_path); + } } SectionTitle("Options");