Files
archived-pcsx2/pcsx2/Recording/VirtualPad/VirtualPad.cpp
Tyler Wilding 400ed82715 recording: Many adjustments on VirtualPad from review feedback
recording: Factor in DPIScaling when scaling VirtualPad window


recording: Add a reset button, hopefully better quality backgrounds for lower resolutions


recording: Improved VPad analog alignment

Co-authored-by: sonicfind
recording: Some final VirtualPad coordinate adjustments
2020-10-01 17:52:59 +01:00

439 lines
19 KiB
C++

/* PCSX2 - PS2 Emulator for PCs
* Copyright (C) 2002-2020 PCSX2 Dev Team
*
* PCSX2 is free software: you can redistribute it and/or modify it under the terms
* of the GNU Lesser General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* PCSX2 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 PCSX2.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include "PrecompiledHeader.h"
#ifndef DISABLE_RECORDING
#include <math.h>
#include "App.h"
#include "MSWstuff.h"
#include "Utilities/EmbeddedImage.h"
#include "wx/dcbuffer.h"
#include "wx/display.h"
#include "wx/spinctrl.h"
#include "Recording/VirtualPad/VirtualPad.h"
#include "Recording/VirtualPad/VirtualPadResources.h"
#include "Recording/VirtualPad/img/circlePressed.h"
#include "Recording/VirtualPad/img/controllerFull.h"
#include "Recording/VirtualPad/img/controllerThreeQuarters.h"
#include "Recording/VirtualPad/img/controllerHalf.h"
#include "Recording/VirtualPad/img/crossPressed.h"
#include "Recording/VirtualPad/img/downPressed.h"
#include "Recording/VirtualPad/img/l1Pressed.h"
#include "Recording/VirtualPad/img/l2Pressed.h"
#include "Recording/VirtualPad/img/l3Pressed.h"
#include "Recording/VirtualPad/img/leftPressed.h"
#include "Recording/VirtualPad/img/r1Pressed.h"
#include "Recording/VirtualPad/img/r2Pressed.h"
#include "Recording/VirtualPad/img/r3Pressed.h"
#include "Recording/VirtualPad/img/rightPressed.h"
#include "Recording/VirtualPad/img/selectPressed.h"
#include "Recording/VirtualPad/img/squarePressed.h"
#include "Recording/VirtualPad/img/startPressed.h"
#include "Recording/VirtualPad/img/trianglePressed.h"
#include "Recording/VirtualPad/img/upPressed.h"
VirtualPad::VirtualPad(wxWindow* parent, int controllerPort, AppConfig::InputRecordingOptions& options)
: wxFrame(parent, wxID_ANY, wxEmptyString)
, options(options)
{
// Images at 1.00 scale are designed to work well on HiDPI (4k) at 150% scaling (default recommended setting on windows)
// Therefore, on a 1080p monitor we halve the scaling, on 1440p we reduce it by 25%, which from some quick tests looks comparable
// Side-note - Getting the DPI scaling amount is platform specific (with some platforms only supporting
// integer scaling as well) this is likely not reliable.
// Slight multi-monitor support, will use whatever window pcsx2 is opened with, but won't currently re-init if
// windows are dragged between differing monitors!
wxDisplay display(wxDisplay::GetFromWindow(this));
const wxRect screen = display.GetClientArea();
float dpiScale = MSW_GetDPIScale(); // linux returns 1.0
if (screen.height > 1080 && screen.height <= 1440) // 1440p display
scalingFactor = 0.75 * dpiScale;
else if (screen.height <= 1080) // 1080p display
{
scalingFactor = 0.5 * dpiScale;
}
// otherwise use default 1.0 scaling
virtualPadData = VirtualPadData();
// Based on the scaling factor, select the appropriate background image
// Don't scale these images as they've already been pre-scaled
if (floatCompare(scalingFactor, 0.5))
virtualPadData.background = NewBitmap(EmbeddedImage<res_controllerHalf>().Get(), wxPoint(0, 0), true);
else if (floatCompare(scalingFactor, 0.75))
virtualPadData.background = NewBitmap(EmbeddedImage<res_controllerThreeQuarters>().Get(), wxPoint(0, 0), true);
else
// Otherwise, scale down/up (or don't in the case of 1.0) the largst image
virtualPadData.background = NewBitmap(EmbeddedImage<res_controllerFull>().Get(), wxPoint(0, 0));
// Use the background image's size to define the window size
SetClientSize(virtualPadData.background.width, virtualPadData.background.height);
// These hard-coded pixels correspond to where the background image's components are (ie. the buttons)
// Everything is automatically scaled and adjusted based on the `scalingFactor` variable
InitPressureButtonGuiElements(virtualPadData.square, NewBitmap(EmbeddedImage<res_squarePressed>().Get(), wxPoint(852, 287)), this, wxPoint(1055, 525));
InitPressureButtonGuiElements(virtualPadData.triangle, NewBitmap(EmbeddedImage<res_trianglePressed>().Get(), wxPoint(938, 201)), this, wxPoint(1055, 565));
InitPressureButtonGuiElements(virtualPadData.circle, NewBitmap(EmbeddedImage<res_circlePressed>().Get(), wxPoint(1024, 286)), this, wxPoint(1055, 605));
InitPressureButtonGuiElements(virtualPadData.cross, NewBitmap(EmbeddedImage<res_crossPressed>().Get(), wxPoint(938, 369)), this, wxPoint(1055, 645));
InitPressureButtonGuiElements(virtualPadData.left, NewBitmap(EmbeddedImage<res_leftPressed>().Get(), wxPoint(110, 303)), this, wxPoint(175, 525), true);
InitPressureButtonGuiElements(virtualPadData.up, NewBitmap(EmbeddedImage<res_upPressed>().Get(), wxPoint(186, 227)), this, wxPoint(175, 565), true);
InitPressureButtonGuiElements(virtualPadData.right, NewBitmap(EmbeddedImage<res_rightPressed>().Get(), wxPoint(248, 302)), this, wxPoint(175, 605), true);
InitPressureButtonGuiElements(virtualPadData.down, NewBitmap(EmbeddedImage<res_downPressed>().Get(), wxPoint(186, 359)), this, wxPoint(175, 645), true);
InitPressureButtonGuiElements(virtualPadData.l1, NewBitmap(EmbeddedImage<res_l1Pressed>().Get(), wxPoint(156, 98)), this, wxPoint(170, 135));
InitPressureButtonGuiElements(virtualPadData.l2, NewBitmap(EmbeddedImage<res_l2Pressed>().Get(), wxPoint(156, 57)), this, wxPoint(170, 52), false, true);
InitPressureButtonGuiElements(virtualPadData.r1, NewBitmap(EmbeddedImage<res_r1Pressed>().Get(), wxPoint(921, 98)), this, wxPoint(1035, 135), true);
InitPressureButtonGuiElements(virtualPadData.r2, NewBitmap(EmbeddedImage<res_r2Pressed>().Get(), wxPoint(921, 57)), this, wxPoint(1035, 52), true, true);
InitNormalButtonGuiElements(virtualPadData.select, NewBitmap(EmbeddedImage<res_selectPressed>().Get(), wxPoint(458, 313)), this, wxPoint(530, 315));
InitNormalButtonGuiElements(virtualPadData.start, NewBitmap(EmbeddedImage<res_startPressed>().Get(), wxPoint(688, 311)), this, wxPoint(646, 315));
InitNormalButtonGuiElements(virtualPadData.l3, NewBitmap(EmbeddedImage<res_l3Pressed>().Get(), wxPoint(336, 453)), this, wxPoint(560, 638));
InitNormalButtonGuiElements(virtualPadData.r3, NewBitmap(EmbeddedImage<res_r3Pressed>().Get(), wxPoint(726, 453)), this, wxPoint(615, 638));
InitAnalogStickGuiElements(virtualPadData.leftAnalog, this, wxPoint(404, 522), 100, wxPoint(314, 642), wxPoint(526, 432), false, wxPoint(504, 685), wxPoint(570, 425), true);
InitAnalogStickGuiElements(virtualPadData.rightAnalog, this, wxPoint(794, 522), 100, wxPoint(706, 642), wxPoint(648, 432), true, wxPoint(700, 685), wxPoint(635, 425));
ignoreRealControllerBox = new wxCheckBox(this, wxID_ANY, wxEmptyString, ScaledPoint(wxPoint(586, 135)), wxDefaultSize);
resetButton = new wxButton(this, wxID_ANY, _("Reset"), ScaledPoint(wxPoint(1195, 5), wxSize(100, 50), true), ScaledSize(wxSize(100, 50)));
Bind(wxEVT_CHECKBOX, &VirtualPad::OnIgnoreRealController, this, ignoreRealControllerBox->GetId());
Bind(wxEVT_BUTTON, &VirtualPad::OnResetButton, this, resetButton->GetId());
// Bind Window Events
Bind(wxEVT_MOVE, &VirtualPad::OnMoveAround, this);
Bind(wxEVT_CLOSE_WINDOW, &VirtualPad::OnClose, this);
Bind(wxEVT_ICONIZE, &VirtualPad::OnIconize, this);
Bind(wxEVT_ERASE_BACKGROUND, &VirtualPad::OnEraseBackground, this);
// Temporary Paint event handler so the window displays properly before the controller-interrupt routine takes over with manual drawing.
// The reason for this is in order to minimize the performance impact, we need total control over when render is called
// Windows redraws the window _alot_ otherwise which leads to major performance problems (when GS is using the software renderer)
Bind(wxEVT_PAINT, &VirtualPad::OnPaint, this);
// DevCon.WriteLn("Paint Event Bound");
// Finalize layout
SetIcons(wxGetApp().GetIconBundle());
SetTitle(wxString::Format("Virtual Pad - Port %d", controllerPort + 1));
SetPosition(options.VirtualPadPosition);
SetBackgroundColour(*wxWHITE);
SetBackgroundStyle(wxBG_STYLE_PAINT);
// This window does not allow for resizing for sake of simplicity: all images are scaled initially and stored, ready to be rendered
SetWindowStyle(wxDEFAULT_FRAME_STYLE & ~wxRESIZE_BORDER & ~wxMAXIMIZE_BOX);
// Causes flickering, despite it supposed to be preventing it!
// SetDoubleBuffered(true);
}
void VirtualPad::OnMoveAround(wxMoveEvent& event)
{
if (IsBeingDeleted() || !IsVisible() || IsIconized())
return;
if (!IsMaximized())
options.VirtualPadPosition = GetPosition();
event.Skip();
}
void VirtualPad::OnClose(wxCloseEvent& event)
{
// Re-bind the Paint event in case this is due to a game being opened/closed
manualRedrawMode = false;
Bind(wxEVT_PAINT, &VirtualPad::OnPaint, this);
// DevCon.WriteLn("Paint Event Bound");
Hide();
}
void VirtualPad::OnIconize(wxIconizeEvent& event)
{
if (event.IsIconized())
{
manualRedrawMode = false;
Bind(wxEVT_PAINT, &VirtualPad::OnPaint, this);
// DevCon.WriteLn("Paint Event Bound");
}
}
void VirtualPad::OnEraseBackground(wxEraseEvent& event)
{
// Intentionally Empty
// See - https://wiki.wxwidgets.org/Flicker-Free_Drawing
}
void VirtualPad::OnPaint(wxPaintEvent& event)
{
// DevCon.WriteLn("Paint Event Called");
wxBufferedPaintDC dc(this, wxBUFFER_VIRTUAL_AREA);
Render(dc);
}
void VirtualPad::Redraw()
{
wxClientDC cdc(this);
wxBufferedDC dc(&cdc);
Render(dc);
}
void VirtualPad::Render(wxDC& bdc)
{
// Update GUI Elements and figure out what needs to be rendered
for (VirtualPadElement* virtualPadElement : virtualPadElements)
virtualPadElement->UpdateGuiElement(renderQueue, clearScreenRequired);
// Update Graphic Elements off render stack
// Before we start rendering (if we have to) clear and re-draw the background
if (!manualRedrawMode || clearScreenRequired || !renderQueue.empty())
{
bdc.SetBrush(*wxWHITE);
bdc.DrawRectangle(wxPoint(0, 0), bdc.GetSize());
bdc.SetBrush(wxNullBrush);
bdc.DrawBitmap(virtualPadData.background.image, virtualPadData.background.coords, true);
clearScreenRequired = false;
// Switch to Manual Rendering once the first user action on the VirtualPad is taken
if (!manualRedrawMode && !renderQueue.empty())
{
// DevCon.WriteLn("Paint Event Unbound");
Unbind(wxEVT_PAINT, &VirtualPad::OnPaint, this);
manualRedrawMode = true;
}
// NOTE - there is yet another (and I think final) micro-optimization that can be done:
// It can be assumed that if the element has already been drawn to the screen (and not cleared) that we can skip rendering it
//
// For example - you hold a single button for several frames, it will currently draw that every frame
// despite the screen never being cleared - so this is not strictly necessary.
//
// Though after some tests, the performance impact is well within reason, and on the hardware renderer modes, is almost non-existant.
while (!renderQueue.empty())
{
VirtualPadElement* element = renderQueue.front();
if (element)
element->Render(bdc);
renderQueue.pop();
}
}
}
bool VirtualPad::UpdateControllerData(u16 const bufIndex, PadData* padData)
{
return virtualPadData.UpdateVirtualPadData(bufIndex, padData, ignoreRealController && !readOnlyMode, readOnlyMode);
}
void VirtualPad::enableUiElements(bool enable)
{
ignoreRealControllerBox->Enable(enable);
resetButton->Enable(enable);
for (VirtualPadElement* virtualPadElement : virtualPadElements)
virtualPadElement->EnableWidgets(enable);
}
void VirtualPad::SetReadOnlyMode(bool readOnly)
{
enableUiElements(!readOnly);
readOnlyMode = readOnly;
}
void VirtualPad::OnIgnoreRealController(wxCommandEvent& event)
{
const wxCheckBox* ignoreButton = (wxCheckBox*)event.GetEventObject();
if (ignoreButton)
ignoreRealController = ignoreButton->GetValue();
}
void VirtualPad::OnResetButton(wxCommandEvent& event)
{
if (readOnlyMode)
return;
for (VirtualPadElement* virtualPadElement : virtualPadElements)
virtualPadElement->Reset(this);
}
void VirtualPad::OnNormalButtonPress(wxCommandEvent& event)
{
const wxCheckBox* pressedButton = (wxCheckBox*)event.GetEventObject();
ControllerNormalButton* eventBtn = buttonElements[pressedButton->GetId()];
if (pressedButton)
eventBtn->pressed = pressedButton->GetValue();
if (!eventBtn->isControllerPressBypassed)
eventBtn->isControllerPressBypassed = true;
}
void VirtualPad::OnPressureButtonPressureChange(wxCommandEvent& event)
{
const wxSpinCtrl* pressureSpinner = (wxSpinCtrl*)event.GetEventObject();
ControllerPressureButton* eventBtn = pressureElements[pressureSpinner->GetId()];
if (pressureSpinner)
eventBtn->pressure = pressureSpinner->GetValue();
eventBtn->pressed = eventBtn->pressure > 0;
if (!eventBtn->isControllerPressureBypassed || !eventBtn->isControllerPressBypassed)
{
eventBtn->isControllerPressureBypassed = true;
eventBtn->isControllerPressBypassed = true;
}
}
void VirtualPad::OnAnalogSpinnerChange(wxCommandEvent& event)
{
const wxSpinCtrl* analogSpinner = (wxSpinCtrl*)event.GetEventObject();
AnalogVector* eventVector = analogElements[analogSpinner->GetId()];
if (analogSpinner)
eventVector->val = analogSpinner->GetValue();
eventVector->slider->SetValue(eventVector->val);
if (!eventVector->isControllerBypassed)
eventVector->isControllerBypassed = true;
}
void VirtualPad::OnAnalogSliderChange(wxCommandEvent& event)
{
const wxSlider* analogSlider = (wxSlider*)event.GetEventObject();
AnalogVector* eventVector = analogElements[analogSlider->GetId()];
if (analogSlider)
eventVector->val = analogSlider->GetValue();
eventVector->spinner->SetValue(eventVector->val);
if (!eventVector->isControllerBypassed)
eventVector->isControllerBypassed = true;
}
/// GUI Element Utility Functions
bool VirtualPad::floatCompare(float a, float b, float epsilon)
{
return (fabs(a - b) < epsilon);
}
wxPoint VirtualPad::ScaledPoint(wxPoint point, wxSize widgetWidth, bool rightAlignedCoord, bool bottomAlignedCoord)
{
return ScaledPoint(point.x, point.y, widgetWidth.x, widgetWidth.y, rightAlignedCoord, bottomAlignedCoord);
}
wxPoint VirtualPad::ScaledPoint(int x, int y, int widgetWidth, int widgetHeight, bool rightAlignedCoord, bool bottomAlignedCoord)
{
wxPoint scaledPoint = wxPoint(x * scalingFactor, y * scalingFactor);
if (rightAlignedCoord)
{
scaledPoint.x -= widgetWidth * scalingFactor;
if (scaledPoint.x < 0)
scaledPoint.x = 0;
}
if (bottomAlignedCoord)
{
scaledPoint.y -= widgetHeight * scalingFactor;
if (scaledPoint.y < 0)
scaledPoint.y = 0;
}
return scaledPoint;
}
wxSize VirtualPad::ScaledSize(wxSize size)
{
return ScaledSize(size.x, size.y);
}
wxSize VirtualPad::ScaledSize(int x, int y)
{
return wxSize(x * scalingFactor, y * scalingFactor);
}
ImageFile VirtualPad::NewBitmap(wxImage resource, wxPoint imgCoord, bool dontScale)
{
return NewBitmap(dontScale ? 1 : scalingFactor, resource, imgCoord);
}
ImageFile VirtualPad::NewBitmap(float scalingFactor, wxImage resource, wxPoint imgCoord)
{
wxBitmap bitmap = wxBitmap(resource.Rescale(resource.GetWidth() * scalingFactor, resource.GetHeight() * scalingFactor, wxIMAGE_QUALITY_HIGH));
ImageFile image = ImageFile();
image.image = bitmap;
image.width = bitmap.GetWidth();
image.height = bitmap.GetHeight();
image.coords = ScaledPoint(imgCoord);
return image;
}
void VirtualPad::InitNormalButtonGuiElements(ControllerNormalButton& button, ImageFile image, wxWindow* parentWindow, wxPoint checkboxCoord)
{
button.icon = image;
button.pressedBox = new wxCheckBox(parentWindow, wxID_ANY, wxEmptyString, ScaledPoint(checkboxCoord), wxDefaultSize);
Bind(wxEVT_CHECKBOX, &VirtualPad::OnNormalButtonPress, this, button.pressedBox->GetId());
buttonElements[button.pressedBox->GetId()] = &button;
virtualPadElements.push_back(&button);
}
void VirtualPad::InitPressureButtonGuiElements(ControllerPressureButton& button, ImageFile image, wxWindow* parentWindow, wxPoint pressureSpinnerCoord, bool rightAlignedCoord, bool bottomAlignedCoord)
{
const wxPoint scaledPoint = ScaledPoint(pressureSpinnerCoord, SPINNER_SIZE, rightAlignedCoord, bottomAlignedCoord);
wxSpinCtrl* spinner = new wxSpinCtrl(parentWindow, wxID_ANY, wxEmptyString, scaledPoint, ScaledSize(SPINNER_SIZE), wxSP_ARROW_KEYS, 0, 255, 0);
button.icon = image;
button.pressureSpinner = spinner;
Bind(wxEVT_SPINCTRL, &VirtualPad::OnPressureButtonPressureChange, this, button.pressureSpinner->GetId());
pressureElements[button.pressureSpinner->GetId()] = &button;
virtualPadElements.push_back(&button);
}
void VirtualPad::InitAnalogStickGuiElements(AnalogStick& analog, wxWindow* parentWindow, wxPoint centerPoint, int radius,
wxPoint xSliderPoint, wxPoint ySliderPoint, bool flipYSlider, wxPoint xSpinnerPoint, wxPoint ySpinnerPoint, bool rightAlignedSpinners)
{
AnalogPosition analogPos = AnalogPosition();
analogPos.centerCoords = ScaledPoint(centerPoint);
analogPos.endCoords = ScaledPoint(centerPoint);
analogPos.radius = radius * scalingFactor;
analogPos.lineThickness = 6 * scalingFactor;
const wxPoint xSpinnerScaledPoint = ScaledPoint(xSpinnerPoint, SPINNER_SIZE, rightAlignedSpinners);
const wxPoint ySpinnerScaledPoint = ScaledPoint(ySpinnerPoint, SPINNER_SIZE, rightAlignedSpinners, true);
wxSlider* xSlider = new wxSlider(parentWindow, wxID_ANY, ANALOG_NEUTRAL, 0, ANALOG_MAX,
ScaledPoint(xSliderPoint), ScaledSize(ANALOG_SLIDER_WIDTH, ANALOG_SLIDER_HEIGHT));
wxSlider* ySlider = new wxSlider(parentWindow, wxID_ANY, ANALOG_NEUTRAL, 0, ANALOG_MAX,
ScaledPoint(ySliderPoint), ScaledSize(ANALOG_SLIDER_HEIGHT, ANALOG_SLIDER_WIDTH), flipYSlider ? wxSL_LEFT : wxSL_RIGHT);
wxSpinCtrl* xSpinner = new wxSpinCtrl(parentWindow, wxID_ANY, wxEmptyString, xSpinnerScaledPoint, ScaledSize(SPINNER_SIZE), wxSP_ARROW_KEYS, 0, 255, 127);
wxSpinCtrl* ySpinner = new wxSpinCtrl(parentWindow, wxID_ANY, wxEmptyString, ySpinnerScaledPoint, ScaledSize(SPINNER_SIZE), wxSP_ARROW_KEYS, 0, 255, 127);
analog.xVector.slider = xSlider;
analog.yVector.slider = ySlider;
analog.xVector.spinner = xSpinner;
analog.yVector.spinner = ySpinner;
analog.positionGraphic = analogPos;
Bind(wxEVT_SLIDER, &VirtualPad::OnAnalogSliderChange, this, xSlider->GetId());
Bind(wxEVT_SLIDER, &VirtualPad::OnAnalogSliderChange, this, ySlider->GetId());
Bind(wxEVT_SPINCTRL, &VirtualPad::OnAnalogSpinnerChange, this, xSpinner->GetId());
Bind(wxEVT_SPINCTRL, &VirtualPad::OnAnalogSpinnerChange, this, ySpinner->GetId());
analogElements[xSlider->GetId()] = &analog.xVector;
analogElements[ySlider->GetId()] = &analog.yVector;
analogElements[xSpinner->GetId()] = &analog.xVector;
analogElements[ySpinner->GetId()] = &analog.yVector;
virtualPadElements.push_back(&analog);
}
#endif