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

Slot-2 Motion Pak, Guitar Grip emulation #2183

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_library(core STATIC
FATStorage.cpp
FIFO.h
GBACart.cpp
GBACartMotionPak.cpp
GPU.cpp
GPU2D.cpp
GPU2D_Soft.cpp
Expand Down
30 changes: 30 additions & 0 deletions src/GBACart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,27 @@ void CartRumblePak::ROMWrite(u32 addr, u16 val)
}
}

CartGuitarGrip::CartGuitarGrip(void* userdata) :
CartCommon(GuitarGrip),
UserData(userdata)
{
}

CartGuitarGrip::~CartGuitarGrip() = default;

u16 CartGuitarGrip::ROMRead(u32 addr) const
{
return 0xF9FF;
}

u8 CartGuitarGrip::SRAMRead(u32 addr)
{
return ~((Platform::Addon_KeyDown(Platform::KeyGuitarGripGreen, UserData) ? 0x40 : 0)
| (Platform::Addon_KeyDown(Platform::KeyGuitarGripRed, UserData) ? 0x20 : 0)
| (Platform::Addon_KeyDown(Platform::KeyGuitarGripYellow, UserData) ? 0x10 : 0)
| (Platform::Addon_KeyDown(Platform::KeyGuitarGripBlue, UserData) ? 0x08 : 0));
}

GBACartSlot::GBACartSlot(melonDS::NDS& nds, std::unique_ptr<CartCommon>&& cart) noexcept : NDS(nds), Cart(std::move(cart))
{
}
Expand Down Expand Up @@ -843,6 +864,15 @@ std::unique_ptr<CartCommon> LoadAddon(int type, void* userdata)
case GBAAddon_RumblePak:
cart = std::make_unique<CartRumblePak>(userdata);
break;
case GBAAddon_MotionPakHomebrew:
cart = std::make_unique<CartMotionPakHomebrew>(userdata);
break;
case GBAAddon_MotionPakRetail:
cart = std::make_unique<CartMotionPakRetail>(userdata);
break;
case GBAAddon_GuitarGrip:
cart = std::make_unique<CartGuitarGrip>(userdata);
break;

default:
Log(LogLevel::Warn, "GBACart: !! invalid addon type %d\n", type);
Expand Down
60 changes: 60 additions & 0 deletions src/GBACart.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ enum CartType
GameSolarSensor = 0x102,
RAMExpansion = 0x201,
RumblePak = 0x202,
MotionPakHomebrew = 0x203,
MotionPakRetail = 0x204,
GuitarGrip = 0x205,
};

// CartCommon -- base code shared by all cart types
Expand Down Expand Up @@ -211,11 +214,68 @@ class CartRumblePak : public CartCommon
u16 RumbleState = 0;
};

// CartGuitarGrip -- DS Guitar Grip (used in various NDS games)
class CartGuitarGrip : public CartCommon
{
public:
CartGuitarGrip(void* userdata);
~CartGuitarGrip() override;

u16 ROMRead(u32 addr) const override;
u8 SRAMRead(u32 addr) override;

private:
void* UserData;
};

// CartMotionPakHomebrew -- DS Motion Pak (Homebrew)
class CartMotionPakHomebrew : public CartCommon
{
public:
CartMotionPakHomebrew(void* userdata);
~CartMotionPakHomebrew() override;

void Reset() override;

void DoSavestate(Savestate* file) override;

u16 ROMRead(u32 addr) const override;
u8 SRAMRead(u32 addr) override;

private:
void* UserData;
u16 ShiftVal = 0;
};

// CartMotionPakRetail -- DS Motion Pack (Retail)
class CartMotionPakRetail : public CartCommon
{
public:
CartMotionPakRetail(void* userdata);
~CartMotionPakRetail() override;

void Reset() override;

void DoSavestate(Savestate* file) override;

u16 ROMRead(u32 addr) const override;
u8 SRAMRead(u32 addr) override;

private:
void* UserData;
u8 Value;
u8 Step = 16;
};

// possible inputs for GBA carts that might accept user input
enum
{
Input_SolarSensorDown = 0,
Input_SolarSensorUp,
Input_GuitarGripGreen,
Input_GuitarGripRed,
Input_GuitarGripYellow,
Input_GuitarGripBlue,
};

class GBACartSlot
Expand Down
196 changes: 196 additions & 0 deletions src/GBACartMotionPak.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
Copyright 2016-2024 melonDS team

This file is part of melonDS.

melonDS 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 3 of the License, or (at your option)
any later version.

melonDS 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 melonDS. If not, see http://www.gnu.org/licenses/.
*/

#include <assert.h>
#include "NDS.h"
#include "GBACart.h"
#include "Platform.h"
#include <algorithm>
#include "math.h"

namespace melonDS
{
using Platform::Log;
using Platform::LogLevel;

namespace GBACart
{

CartMotionPakHomebrew::CartMotionPakHomebrew(void* userdata) :
CartCommon(MotionPakHomebrew),
UserData(userdata)
{
}

CartMotionPakHomebrew::~CartMotionPakHomebrew() = default;

void CartMotionPakHomebrew::Reset()
{
ShiftVal = 0;
}

void CartMotionPakHomebrew::DoSavestate(Savestate* file)
{
CartCommon::DoSavestate(file);
file->Var16(&ShiftVal);
}

u16 CartMotionPakHomebrew::ROMRead(u32 addr) const
{
// CHECKME: Does this apply to the homebrew cart as well?
return 0xFCFF;
}

static int AccelerationToMotionPak(float accel)
{
const float GRAVITY_M_S2 = 9.80665f;

return std::clamp(
(int) ((accel / (5 * GRAVITY_M_S2) + 0.5) * 4096),
0, 4095
);
}

static int AccelerationToMotionPakRetail(float accel)
{
const float GRAVITY_M_S2 = 9.80665f;

return std::clamp(
(int) ((accel / (5 * GRAVITY_M_S2) + 0.5) * 256),
0, 254
);
}

static int RotationToMotionPak(float rot)
{
const float DEGREES_PER_RAD = 180 / M_PI;
const float COUNTS_PER_DEG_PER_SEC = 0.825;
const int CENTER = 1680;

return std::clamp(
(int) ((rot * DEGREES_PER_RAD * COUNTS_PER_DEG_PER_SEC) + CENTER + 0.5),
0, 4095
);
}

u8 CartMotionPakHomebrew::SRAMRead(u32 addr)
{
// CHECKME: SRAM address mask
addr &= 0xFFFF;

switch (addr)
{
case 0:
// Read next byte
break;
case 2:
// Read X acceleration
ShiftVal = AccelerationToMotionPak(Platform::Addon_MotionQuery(Platform::MotionAccelerationX, UserData)) << 4;
// CHECKME: First byte returned when reading acceleration/rotation
return 0;
case 4:
// Read Y acceleration
ShiftVal = AccelerationToMotionPak(Platform::Addon_MotionQuery(Platform::MotionAccelerationY, UserData)) << 4;
return 0;
case 6:
// Read Z acceleration
ShiftVal = AccelerationToMotionPak(Platform::Addon_MotionQuery(Platform::MotionAccelerationZ, UserData)) << 4;
return 0;
case 8:
// Read Z rotation
// CHECKME: This is a guess, compare with real hardware
Copy link
Contributor

Choose a reason for hiding this comment

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

You may have a hard time here as I'm not entirely convinced the homebrew Slot2 motion pack was ever actually sold/made available. The only product I can find with a motion pack is supposedly the Neoflash "R6Gold".

All of my knowledge of the homebrew cart's spec comes from libnds.

Copy link
Contributor Author

@asiekierka asiekierka Nov 5, 2024

Choose a reason for hiding this comment

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

You may have a hard time here as I'm not entirely convinced the homebrew Slot2 motion pack was ever actually sold/made available.

Absolutely was. https://web.archive.org/web/20080509173200/http://www.ndsmotion.com/ has a list of stores and photos of the packaging.

The problem is that these types of products (the DSerial Edge has a similar problem) were predominantly popular with homebrew developers themselves, so not only did they probably sell in highly limited qualities, they're unlikely to pop up on the second-hand market due to personal nostalgia.

There were six products in total which featured Kionix's technology, five of which were for the DS family of consoles; I maintain a list here: https://wiki.asie.pl/doku.php?id=notes:flashcart:ds_motion_card - of those, the GBAccelerometer is the easiest to find (it's still available in stock in 2024), but that's not a DS product; next, there's the Activision pak, and after that probably the R6 Gold?

I tested with an MK6-Motion myself, which happens to feature both the accelerometer and gyroscope chip; since the chips themselves and thus values output should be identical between the Card and a Pak, I deemed the emulation to be accurate enough for running homebrew (which is the important thing for me from a preservation perspective), but I can't verify fine details like the Pak's open bus behaviour or any additional SPI/ROM commands the Card might have.

All of my knowledge of the homebrew cart's spec comes from libnds.

There's some additional bits of information in a README bundled with the original DS Motion Card/Pak driver source code on said website.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Either way, I don't expect this to be resolved as part of this PR, and the detection mechanism used by all homebrew for the Pak doesn't rely on open bus behaviour.)

ShiftVal = RotationToMotionPak(-Platform::Addon_MotionQuery(Platform::MotionRotationZ, UserData)) << 4;
return 0;
case 10:
// Identify cart
ShiftVal = 0xF00F;
return 0;
case 12:
case 14:
case 16:
case 18:
// Read/enable analog inputs
//
// These are not connected by defualt and require do-it-yourself cart
// modification, so there is no reason to emulate them.
ShiftVal = 0;
break;
}

// Read high byte from the emulated shift register
u8 val = ShiftVal >> 8;
ShiftVal <<= 8;
return val;
}

CartMotionPakRetail::CartMotionPakRetail(void* userdata) :
CartCommon(MotionPakRetail),
UserData(userdata)
{
}

CartMotionPakRetail::~CartMotionPakRetail() = default;

void CartMotionPakRetail::Reset()
{
Value = 0;
Step = 16;
}

void CartMotionPakRetail::DoSavestate(Savestate* file)
{
CartCommon::DoSavestate(file);
file->Var8(&Value);
file->Var8(&Step);
}

u16 CartMotionPakRetail::ROMRead(u32 addr) const
{
// A9-A8 is pulled low on a real Motion Pack.
return 0xFCFF;
}

u8 CartMotionPakRetail::SRAMRead(u32 addr)
{
switch (Step)
{
case 0: // Synchronization - read 0xFF
Value = 0xFF;
break;
case 4: // X acceleration
Value = AccelerationToMotionPakRetail(Platform::Addon_MotionQuery(Platform::MotionAccelerationX, UserData));
break;
case 8: // Y acceleration
Value = AccelerationToMotionPakRetail(Platform::Addon_MotionQuery(Platform::MotionAccelerationY, UserData));
break;
case 12: // Z acceleration
Value = AccelerationToMotionPakRetail(Platform::Addon_MotionQuery(Platform::MotionAccelerationZ, UserData));
break;
case 16: // Synchronization - read 0b00
Step = 0;
return 0;
}

int shift = 6 - ((Step & 3) * 2);
Step++;
return (Value >> shift) & 0x03;
}

}

}
3 changes: 3 additions & 0 deletions src/NDS.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ enum
{
GBAAddon_RAMExpansion = 1,
GBAAddon_RumblePak = 2,
GBAAddon_MotionPakHomebrew = 3,
GBAAddon_MotionPakRetail = 4,
GBAAddon_GuitarGrip = 5,
};

class SPU;
Expand Down
48 changes: 48 additions & 0 deletions src/Platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,18 @@ void Camera_CaptureFrame(int num, u32* frame, int width, int height, bool yuv, v

// interface for addon inputs

enum KeyType
{
KeyGuitarGripGreen,
KeyGuitarGripRed,
KeyGuitarGripYellow,
KeyGuitarGripBlue,
};

// Check if a given key is being pressed.
// @param type The type of the key to check.
bool Addon_KeyDown(KeyType type, void* userdata);

// Called by the DS Rumble Pak emulation to start the necessary
// rumble effects on the connected game controller, if available.
// @param len The duration of the controller rumble effect in milliseconds.
Expand All @@ -331,6 +343,42 @@ void Addon_RumbleStart(u32 len, void* userdata);
// rumble effects on the connected game controller, if available.
void Addon_RumbleStop(void* userdata);

enum MotionQueryType
{
/**
* @brief X axis acceleration, measured in SI meters per second squared.
* On a DS, the X axis refers to the top screen X-axis (left ... right).
*/
MotionAccelerationX,
/**
* @brief Y axis acceleration, measured in SI meters per second squared.
* On a DS, the Y axis refers to the top screen Y-axis (bottom ... top).
*/
MotionAccelerationY,
/**
* @brief Z axis acceleration, measured in SI meters per second squared.
* On a DS, the Z axis refers to the axis perpendicular to the top screen (farther ... closer).
*/
MotionAccelerationZ,
/**
* @brief X axis rotation, measured in radians per second.
*/
MotionRotationX,
/**
* @brief Y axis rotation, measured in radians per second.
*/
MotionRotationY,
/**
* @brief Z axis rotation, measured in radians per second.
*/
MotionRotationZ,
};

// Called by the DS Motion Pak emulation to query the game controller's
// aceelration and rotation, if available.
// @param type The value being queried.
float Addon_MotionQuery(MotionQueryType type, void* userdata);

struct DynamicLibrary;

/**
Expand Down
Loading
Loading