Triforce: map touchscreen input to the full touch surface

The Triforce touchscreen path currently derives its coordinates from the shaped
GC C-stick bytes. That keeps cardinal directions reachable, but leaves the outer
corner area inaccessible because the stick still gets bounded like a GC substick.

Issue:
The Key Of Avalon could reach full left/right/up/down, but diagonal and outer
corner areas remained unreachable.

Test:
Boot The Key Of Avalon 1.3 - Chaotic Sabbat (client, Rev C) and move the touch
cursor to the screen corners. The outer edge is now reachable, while R-Analog
still acts as touch pressure.

Read the raw C-stick state for the touchscreen device, expand the circular stick
travel to a square touch surface, and encode that into the packet instead of
using the already-shaped substick bytes.
This commit is contained in:
Francsco "Dil3mm4" Manzo 2026-03-29 15:49:17 +02:00
parent 67f1afeb74
commit 9f76f04395

View File

@ -3,11 +3,16 @@
#include "Core/HW/Triforce/Touchscreen.h"
#include <algorithm>
#include <cmath>
#include <numeric>
#include "Common/BitUtils.h"
#include "Common/Logging/Log.h"
#include "Core/HW/GCPad.h"
#include "Core/HW/GCPadEmu.h"
#include "InputCommon/ControllerEmu/ControlGroup/AnalogStick.h"
#include "InputCommon/ControllerEmu/ControllerEmu.h"
#include "InputCommon/GCPadStatus.h"
namespace
@ -27,6 +32,25 @@ struct SmartSetDataPacket
};
#pragma pack(pop)
static_assert(sizeof(SmartSetDataPacket) == 10);
u16 GetTouchAxisValue(ControlState axis_state, u8 center_value)
{
const u8 axis = ControllerEmu::MapFloat<u8>(std::clamp(axis_state, -1.0, 1.0), center_value);
return Common::ExpandValue(u16(axis), 4);
}
// The physical stick still moves in a circle. Expand that circular range before
// encoding the touchscreen packet so the whole touch surface is reachable.
Common::DVec2 ExpandCircleToSquare(ControlState x, ControlState y)
{
const ControlState max_axis = std::max(std::abs(x), std::abs(y));
if (max_axis == 0.0)
return {x, y};
const ControlState radius = std::hypot(x, y);
const ControlState scale = radius / max_axis;
return {std::clamp(x * scale, -1.0, 1.0), std::clamp(y * scale, -1.0, 1.0)};
}
} // namespace
namespace Triforce
@ -46,10 +70,14 @@ void Touchscreen::Update()
// I'm guessing the real hardware doesn't produce ~60hz input perfectly in-sync with SI updates,
// but Avalon doesn't seem to mind.
// We currently feed the touch screen from c-stick and right-trigger just to make it usable.
// TODO: Expose it in a better way.
const auto pad_status = Pad::GetStatus(0);
const auto* const c_stick =
static_cast<ControllerEmu::AnalogStick*>(Pad::GetGroup(0, PadGroup::CStick));
const auto c_stick_state = c_stick->GetReshapableState(false);
const auto touch_state = ExpandCircleToSquare(c_stick_state.x, c_stick_state.y);
const u16 x = GetTouchAxisValue(touch_state.x, GCPadStatus::C_STICK_CENTER_X);
const u16 y = GetTouchAxisValue(touch_state.y, GCPadStatus::C_STICK_CENTER_Y);
// For reference, the game does something like this to scale the values from 0-4095.
// Note the offsets of 4.
@ -59,8 +87,8 @@ void Touchscreen::Update()
// y = s32(480.f - (0.1171875f * y)) + 4;
SmartSetDataPacket packet{
.x = Common::ExpandValue(u16(pad_status.substickX), 4),
.y = Common::ExpandValue(u16(pad_status.substickY), 4),
.x = x,
.y = y,
.pressure = pad_status.triggerRight,
};