Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multitap driver #1611

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions src/mips/psyqo/advancedpad.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*

MIT License

Copyright (c) 2023 PCSX-Redux authors
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

*/

#pragma once

#include <EASTL/functional.h>
#include <stdint.h>

#include "psyqo/application.hh"

namespace psyqo {

/**
* @brief An advanced class to access the pads.
*
* @details This class is meant to be used as a singleton, probably in
* the `Application` derived class. It does not use the BIOS'
* PAD interface. Instead, it uses the SIO interface directly, and
* can therefore support more device types including multitaps. Polling
* alternates between the two pads/ports on each frame.
*/

class AdvancedPad {
public:
enum Pad { Pad1a, Pad1b, Pad1c, Pad1d, Pad2a, Pad2b, Pad2c, Pad2d };
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved

enum Button {
Select = 0,
L3 = 1,
R3 = 2,
Start = 3,
Up = 4,
Right = 5,
Down = 6,
Left = 7,
L2 = 8,
R2 = 9,
L1 = 10,
R1 = 11,
Triangle = 12,
Circle = 13,
Cross = 14,
Square = 15,
};

enum Command : uint8_t {
PadSelect = 0x01,
ReadPad = 0x42, // 'B' Read Buttons AND analog inputs
// Config mode commands
ToggleConfigMode = 0x43, // 'C' Enter/Exit Configuration Mode
SetLED = 0x44, // 'D' Set LED State (analog mode on/off)
GetLED = 0x45, // 'E' Get LED State (and whatever values)
GetMotorInfo = 0x46, // 'F' Allegedly get info about a motor
GetMotorList = 0x47, // 'G' Allegedly get list of motors
GetMotorState = 0x48, // 'H' Allegedly get motor state
GetSupportedModes = 0x4C, // 'L' Allegedly get supported modes
ConfigRequestFormat = 0x4D, // 'M' Allegedly configure poll request format
ConfigResponseFormat = 0x4F, // 'O' Allegedly configure poll response format

// Config mode commands
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved

};

enum PadType : uint8_t {
Mouse = 0x12, // (two button mouse)
NegCon = 0x23, // (steering twist/wheel/paddle)
KonamiLightgun = 0x31, // (IRQ10-type)
DigitalPad = 0x41, // (or analog pad/stick in digital mode; LED=Off)
AnalogStick = 0x53, // (or analog pad in "flight mode"; LED=Green)
NamcoLightgun = 0x63, // (Cinch-type)
AnalogPad = 0x73, // (in normal analog mode; LED=Red)
Multitap = 0x80, // (multiplayer adaptor) (when activated)
Jogcon = 0xe3, // (steering dial)
ConfigMode = 0xf3, // (when in config mode; see rumble command 43h)
None = 0xFF // (no controller connected, pins floating High-Z)
};

struct Event {
enum { PadConnected, PadDisconnected, ButtonPressed, ButtonReleased } type;
Pad pad;
Button button;
};

enum class TapReadMode {
Passthrough,
BufferedTransferAll,
};

/**
* @brief Initializes the pads.
*
* @details This will initialize the pads polling by calling the BIOS'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The details here are actually incorrect, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revised, but possibly lacking in detail.

* interface. This means this method cannot be called from the `prepare`
* method of the `Application` class, but rather from the `start` method
* of the root `Scene` object. Also, there can be interference with
* the BIOS' memory card functions, so this method is explicit to be
* called in the right order.
*/
void initialize();

/**
* @brief Sets the event callback function.
*
* @details The event callback will be called for each pad-related event,
* such as pad connection / disconnection, or button press / release.
* The callback will only be called between frames.
*
* Scenes that are calling `setOnEvent` during their `start` method should
* call `setOnEvent` again in their `teardown` method with the `nullptr`
* value in order to unregister the event callback cleanly.
*
* Only one callback can be registered at a time, so setting a new
* callback will simply remove the previous one.
*
* Careful about what is called from the callback: pushing or popping scenes
* might call into `setOnEvent` as a result, and could end up corrupting
* memory as a result of the callback being deleted while being executed.
*/
void setOnEvent(eastl::function<void(Event)> &&callback) { m_callback = eastl::move(callback); }

/**
* @brief Returns the state of a pad.
*
* @details Returns the state of a pad. The state is a boolean value
* that is `true` if the pad is connected, and `false` otherwise.
*
* @param pad The pad to query.
* @return A boolean value indicating whether the pad is connected.
*/
bool isPadConnected(Pad pad) const { return (m_padData[pad][0] & 0xff) == 0; }

/**
* @brief Returns the state of a button.
*
* @details Returns the state of a button. The state is a boolean value
* that is `true` if the button is pressed, and `false` otherwise.
*
* @param pad The pad to query.
* @param button The button to query.
* @return A boolean value indicating whether the button is pressed.
*/
bool isButtonPressed(Pad pad, Button button) const { return (m_padData[pad][1] & (1 << button)) == 0; }

private:
void busyLoop(uint16_t delay) {
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
uint16_t cycles = 0;
while (++cycles < delay) {
__asm__ volatile("");
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
}
};

void flushRxBuffer();
constexpr uint8_t output_analog(uint8_t ticks);
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
constexpr uint8_t output_multitap(uint8_t ticks);
constexpr uint8_t output_default(uint8_t ticks);
void processChanges(Pad pad);
void readPad();
uint8_t transceive(uint8_t data_out);
bool waitForAck(); // true if ack received, false if timeout

// bool m_configMode[8] = {false, false, false, false, false, false, false, false};
uint16_t m_padData[8][4];
eastl::function<void(Event)> m_callback;
bool m_connected[8] = {false, false};
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
uint16_t m_buttons[8] = {
0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
};
};

} // namespace psyqo
12 changes: 12 additions & 0 deletions src/mips/psyqo/examples/multitap/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
TARGET = multitap
TYPE = ps-exe

SRCS = \
multitap.cpp \

ifeq ($(TEST),true)
CPPFLAGS = -Werror
endif
CXXFLAGS = -std=c++20

include ../../psyqo.mk
150 changes: 150 additions & 0 deletions src/mips/psyqo/examples/multitap/multitap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@

johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
#include <stdint.h>

#include "common/syscalls/syscalls.h"
#include "psyqo/advancedpad.hh"
#include "psyqo/application.hh"
#include "psyqo/font.hh"
#include "psyqo/gpu.hh"
#include "psyqo/hardware/cpu.hh"
#include "psyqo/hardware/hwregs.hh"
#include "psyqo/hardware/sio.hh"
#include "psyqo/kernel.hh"
#include "psyqo/scene.hh"

namespace {

// Our application. The PadTest class will be created statically, and run from `main`.
class PadTest final : public psyqo::Application {
// We will need both methods to properly set up the application.
void prepare() override;
void createScene() override;

public:
// We will store the font here. We're not going to use chained DMA to display
// anything, so we don't need multiple fragments. Only a single one will suffice.
psyqo::Font<1> m_font;

// Our pad reader.
psyqo::AdvancedPad m_input;
};

// We only have a single since to display the status of the pads.
class PadTestScene final : public psyqo::Scene {
// Since there's only a single scene, we won't need to override the `start`
// or `teardown` methods. We will do all the initialization in the application.
int framecount = 1;
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved
int padindex = 0;
johnbaumann marked this conversation as resolved.
Show resolved Hide resolved

void frame() override;

// Couple of small helpers.
void print(int x, int y, bool enabled, const char *text);
void printPadStatus(psyqo::AdvancedPad::Pad pad, int column, const char *name);
void printPadConnectionStatus(psyqo::AdvancedPad::Pad pad, int row, const char *name);
};

PadTest padTest;
PadTestScene padTestScene;

} // namespace

// The application's `prepare` method is the location to initialize and activate the GPU.
// We shouldn't do anything else that touches the hardware here however, because interrupts
// aren't initialized yet. We could also create more objects here, but we don't have any.
void PadTest::prepare() {
psyqo::GPU::Configuration config;
config.set(psyqo::GPU::Resolution::W320)
.set(psyqo::GPU::VideoMode::AUTO)
.set(psyqo::GPU::ColorMode::C15BITS)
.set(psyqo::GPU::Interlace::PROGRESSIVE);
gpu().initialize(config);
}

// The `createScene` method will be called automatically to create the first scene. It will
// never exit, so there's no need to cater for the reentrancy case here, but technically,
// this method _can_ be called multiple times, if the last scene got popped out. This would
// be bad in this case because it'd mean we're initializing the pads, and uploading the system
// font multiple times.
void PadTest::createScene() {
// We don't have a specific font, so let's just use the built-in system one.
m_font.uploadSystemFont(gpu());
// During `createScene`, interrupts are enabled, so it's okay to call `SimplePad::initialize`.
m_input.initialize();
// And finally we call `pushScene` with the address of our one and only scene. This last
// call is mandatory for everything to function properly.
pushScene(&padTestScene);
}

// Using the system font, our display is roughly 40 columns by 15 lines of text. Although the
// font system can draw text at arbitrary positions on the screen, it's a bit easier to consider
// a matrix of characters instead.
void PadTestScene::print(int x, int y, bool enabled, const char *text) {
y += 2;
psyqo::Vertex pos = {{.x = int16_t(x * 8), .y = int16_t(y * 16)}};
// Doing these lazy initializations is great for readability and encapsulation,
// but due to the way C++ works, it'll call into the C++ guard functions every
// time the function gets called, which may be slower than it could if those
// were in fact globals.
static const auto WHITE = psyqo::Color{{.r = 255, .g = 255, .b = 255}};
static const auto GRAY = psyqo::Color{{.r = 48, .g = 48, .b = 48}};
psyqo::Color c = enabled ? WHITE : GRAY;
padTest.m_font.print(padTest.gpu(), text, pos, c);
}
void PadTestScene::printPadConnectionStatus(psyqo::AdvancedPad::Pad pad, int row, const char *name) {
auto &input = padTest.m_input;
print(8, row, input.isPadConnected(pad), name);
}

void PadTestScene::printPadStatus(psyqo::AdvancedPad::Pad pad, int column, const char *name) {
auto &input = padTest.m_input;
print(column + 0, 0, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Start), "Start");
print(column + 0, 1, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Select), "Select");

print(column + 0, 3, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::L1), "L1");
print(column + 0, 4, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::R1), "R1");
print(column + 0, 5, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::L2), "L2");
print(column + 0, 6, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::R2), "R2");
print(column + 0, 7, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::L3), "L3");
print(column + 0, 8, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::R3), "R3");

print(column + 10, 0, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Up), "Up");
print(column + 10, 1, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Down), "Down");
print(column + 10, 2, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Left), "Left");
print(column + 10, 3, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Right), "Right");

print(column + 10, 5, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Cross), "Cross");
print(column + 10, 6, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Circle), "Circle");
print(column + 10, 7, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Square), "Square");
print(column + 10, 8, input.isButtonPressed(pad, psyqo::AdvancedPad::Button::Triangle), "Triangle");
}

// Our rendering function that'll be called periodically.
void PadTestScene::frame() {
padTest.gpu().clear();

if (framecount++ % (60 * 5) == 0 ||
!padTest.m_input.isPadConnected(static_cast<psyqo::AdvancedPad::Pad>(padindex))) {
// Iterate through the pads, stop on next connected pad
while (!(padTest.m_input.isPadConnected(static_cast<psyqo::AdvancedPad::Pad>(++padindex))) && padindex < 8)
;
if (padindex > 7) {
padindex = 0;
}
}

print(7, padindex, true, ">");

printPadConnectionStatus(psyqo::AdvancedPad::Pad1a, 0, "Pad 1a");
printPadConnectionStatus(psyqo::AdvancedPad::Pad1b, 1, "Pad 1b");
printPadConnectionStatus(psyqo::AdvancedPad::Pad1c, 2, "Pad 1c");
printPadConnectionStatus(psyqo::AdvancedPad::Pad1d, 3, "Pad 1d");
printPadConnectionStatus(psyqo::AdvancedPad::Pad2a, 4, "Pad 2a");
printPadConnectionStatus(psyqo::AdvancedPad::Pad2b, 5, "Pad 2b");
printPadConnectionStatus(psyqo::AdvancedPad::Pad2c, 6, "Pad 2c");
printPadConnectionStatus(psyqo::AdvancedPad::Pad2d, 7, "Pad 2d");

printPadStatus(static_cast<psyqo::AdvancedPad::Pad>(padindex), 20, "Pad Status");
}

int main() { return padTest.run(); }
Loading
Loading