Files
test/intern/ghost/intern/GHOST_Wintab.cc
Campbell Barton ebfa7edeb1 Cleanup: use snake case, replace "m_" prefix with "_" suffix
Follow our own C++ conventions for GHOST.
2025-08-16 16:14:18 +10:00

673 lines
20 KiB
C++

/* SPDX-FileCopyrightText: 2021-2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*/
#define _USE_MATH_DEFINES
#include "GHOST_Wintab.hh"
GHOST_Wintab *GHOST_Wintab::loadWintabUnsafe(HWND hwnd)
{
/* Load Wintab library if available. */
auto handle = unique_hmodule(::LoadLibrary("Wintab32.dll"), &::FreeLibrary);
if (!handle) {
return nullptr;
}
/* Get Wintab functions. */
auto info = (GHOST_WIN32_WTInfo)::GetProcAddress(handle.get(), "WTInfoA");
if (!info) {
return nullptr;
}
auto open = (GHOST_WIN32_WTOpen)::GetProcAddress(handle.get(), "WTOpenA");
if (!open) {
return nullptr;
}
auto get = (GHOST_WIN32_WTGet)::GetProcAddress(handle.get(), "WTGetA");
if (!get) {
return nullptr;
}
auto set = (GHOST_WIN32_WTSet)::GetProcAddress(handle.get(), "WTSetA");
if (!set) {
return nullptr;
}
auto close = (GHOST_WIN32_WTClose)::GetProcAddress(handle.get(), "WTClose");
if (!close) {
return nullptr;
}
auto packetsGet = (GHOST_WIN32_WTPacketsGet)::GetProcAddress(handle.get(), "WTPacketsGet");
if (!packetsGet) {
return nullptr;
}
auto queueSizeGet = (GHOST_WIN32_WTQueueSizeGet)::GetProcAddress(handle.get(), "WTQueueSizeGet");
if (!queueSizeGet) {
return nullptr;
}
auto queueSizeSet = (GHOST_WIN32_WTQueueSizeSet)::GetProcAddress(handle.get(), "WTQueueSizeSet");
if (!queueSizeSet) {
return nullptr;
}
auto enable = (GHOST_WIN32_WTEnable)::GetProcAddress(handle.get(), "WTEnable");
if (!enable) {
return nullptr;
}
auto overlap = (GHOST_WIN32_WTOverlap)::GetProcAddress(handle.get(), "WTOverlap");
if (!overlap) {
return nullptr;
}
/* Build Wintab context. */
LOGCONTEXT lc = {0};
if (!info(WTI_DEFSYSCTX, 0, &lc)) {
return nullptr;
}
Coord tablet, system;
extractCoordinates(lc, tablet, system);
modifyContext(lc);
/* The Wintab spec says we must open the context disabled if we are using cursor masks. */
auto hctx = unique_hctx(open(hwnd, &lc, FALSE), close);
if (!hctx) {
return nullptr;
}
/* Wintab provides no way to determine the maximum queue size aside from checking if attempts
* to change the queue size are successful. */
const int maxQueue = 500;
/* < 0 should realistically never happen, but given we cast to size_t later on better safe than
* sorry. */
int queueSize = max(0, queueSizeGet(hctx.get()));
while (queueSize < maxQueue) {
int testSize = min(queueSize + 16, maxQueue);
if (queueSizeSet(hctx.get(), testSize)) {
queueSize = testSize;
}
else {
/* From Windows Wintab Documentation for WTQueueSizeSet:
* "If the return value is zero, the context has no queue because the function deletes the
* original queue before attempting to create a new one. The application must continue
* calling the function with a smaller queue size until the function returns a non - zero
* value."
*
* In our case we start with a known valid queue size and in the event of failure roll
* back to the last valid queue size. The Wintab spec dates back to 16 bit Windows, thus
* assumes memory recently deallocated may not be available, which is no longer a practical
* concern. */
if (!queueSizeSet(hctx.get(), queueSize)) {
/* If a previously valid queue size is no longer valid, there is likely something wrong in
* the Wintab implementation and we should not use it. */
return nullptr;
}
break;
}
}
int sanityQueueSize = queueSizeGet(hctx.get());
WINTAB_PRINTF("HCTX %p %s queueSize: %d, queueSizeGet: %d\n",
hctx.get(),
__func__,
queueSize,
sanityQueueSize);
WINTAB_PRINTF("Loaded Wintab context %p\n", hctx.get());
return new GHOST_Wintab(std::move(handle),
info,
get,
set,
packetsGet,
enable,
overlap,
std::move(hctx),
tablet,
system,
size_t(queueSize));
}
static int access_violation_exception_filter(unsigned int code, LPEXCEPTION_POINTERS pointers)
{
if (code == EXCEPTION_ACCESS_VIOLATION) {
fprintf(stderr,
"Error loading Wintab library: Access Violation at 0x%p: 0x%p, 0x%p\n",
pointers->ExceptionRecord->ExceptionAddress,
(void *)pointers->ExceptionRecord->ExceptionInformation[0],
(void *)pointers->ExceptionRecord->ExceptionInformation[1]);
return EXCEPTION_EXECUTE_HANDLER;
}
return EXCEPTION_CONTINUE_SEARCH;
}
GHOST_Wintab *GHOST_Wintab::loadWintab(HWND hwnd)
{
/* The only way to get the current handler is by seting a new one. */
LPTOP_LEVEL_EXCEPTION_FILTER current_filter = SetUnhandledExceptionFilter(nullptr);
SetUnhandledExceptionFilter(current_filter);
/* __except and __finally cannot be used together, as such a second nested __try block is needed.
*/
__try
{
__try
{
return GHOST_Wintab::loadWintabUnsafe(hwnd);
}
__except (access_violation_exception_filter(GetExceptionCode(), GetExceptionInformation()))
{
}
}
__finally
{
/* Restore our handler in case the Wintab driver replaced it. Huion's driver is known to do
* this.
*/
SetUnhandledExceptionFilter(current_filter);
}
return nullptr;
}
void GHOST_Wintab::modifyContext(LOGCONTEXT &lc)
{
lc.lcPktData = PACKETDATA;
lc.lcPktMode = PACKETMODE;
lc.lcMoveMask = PACKETDATA;
lc.lcOptions |= CXO_CSRMESSAGES | CXO_MESSAGES;
/* Tablet scaling is handled manually because some drivers don't handle HIDPI or multi-display
* correctly; reset tablet scale factors to un-scaled tablet coordinates. */
lc.lcOutOrgX = lc.lcInOrgX;
lc.lcOutOrgY = lc.lcInOrgY;
lc.lcOutExtX = lc.lcInExtX;
lc.lcOutExtY = lc.lcInExtY;
}
void GHOST_Wintab::extractCoordinates(LOGCONTEXT &lc, Coord &tablet, Coord &system)
{
tablet.x.org = lc.lcInOrgX;
tablet.x.ext = lc.lcInExtX;
tablet.y.org = lc.lcInOrgY;
tablet.y.ext = lc.lcInExtY;
system.x.org = lc.lcSysOrgX;
system.x.ext = lc.lcSysExtX;
system.y.org = lc.lcSysOrgY;
/* Wintab maps y origin to the tablet's bottom; invert y to match Windows y origin mapping to the
* screen top. */
system.y.ext = -lc.lcSysExtY;
}
GHOST_Wintab::GHOST_Wintab(unique_hmodule handle,
GHOST_WIN32_WTInfo info,
GHOST_WIN32_WTGet get,
GHOST_WIN32_WTSet set,
GHOST_WIN32_WTPacketsGet packetsGet,
GHOST_WIN32_WTEnable enable,
GHOST_WIN32_WTOverlap overlap,
unique_hctx hctx,
Coord tablet,
Coord system,
size_t queueSize)
: handle_{std::move(handle)},
fp_info_{info},
fp_get_{get},
fp_set_{set},
fp_packets_get_{packetsGet},
fp_enable_{enable},
fp_overlap_{overlap},
context_{std::move(hctx)},
tablet_coord_{tablet},
system_coord_{system},
pkts_{queueSize}
{
fp_info_(WTI_INTERFACE, IFC_NDEVICES, &num_devices_);
WINTAB_PRINTF("Wintab Devices: %d\n", num_devices_);
updateCursorInfo();
/* Debug info. */
printContextDebugInfo();
}
GHOST_Wintab::~GHOST_Wintab()
{
WINTAB_PRINTF("Closing Wintab context %p\n", context_.get());
}
void GHOST_Wintab::enable()
{
fp_enable_(context_.get(), true);
enabled_ = true;
}
void GHOST_Wintab::disable()
{
if (focused_) {
loseFocus();
}
fp_enable_(context_.get(), false);
enabled_ = false;
}
void GHOST_Wintab::gainFocus()
{
fp_overlap_(context_.get(), true);
focused_ = true;
}
void GHOST_Wintab::loseFocus()
{
if (last_tablet_data_.Active != GHOST_kTabletModeNone) {
leaveRange();
}
/* Mouse mode of tablet or display layout may change when Wintab or Window is inactive. Don't
* trust for mouse movement until re-verified. */
coord_trusted_ = false;
fp_overlap_(context_.get(), false);
focused_ = false;
}
void GHOST_Wintab::leaveRange()
{
/* Button state can't be tracked while out of range, reset it. */
buttons_ = 0;
/* Set to none to indicate tablet is inactive. */
last_tablet_data_ = GHOST_TABLET_DATA_NONE;
/* Clear the packet queue. */
fp_packets_get_(context_.get(), pkts_.size(), pkts_.data());
}
void GHOST_Wintab::remapCoordinates()
{
LOGCONTEXT lc = {0};
if (fp_info_(WTI_DEFSYSCTX, 0, &lc)) {
extractCoordinates(lc, tablet_coord_, system_coord_);
modifyContext(lc);
fp_set_(context_.get(), &lc);
}
}
void GHOST_Wintab::updateCursorInfo()
{
AXIS Pressure, Orientation[3];
BOOL pressureSupport = fp_info_(WTI_DEVICES, DVC_NPRESSURE, &Pressure);
max_pressure_ = pressureSupport ? Pressure.axMax : 0;
WINTAB_PRINTF("HCTX %p %s maxPressure: %d\n", context_.get(), __func__, max_pressure_);
BOOL tiltSupport = fp_info_(WTI_DEVICES, DVC_ORIENTATION, &Orientation);
/* Check if tablet supports azimuth [0] and altitude [1], encoded in axResolution. */
if (tiltSupport && Orientation[0].axResolution && Orientation[1].axResolution) {
max_azimuth_ = Orientation[0].axMax;
max_altitude_ = Orientation[1].axMax;
}
else {
max_azimuth_ = max_altitude_ = 0;
}
WINTAB_PRINTF("HCTX %p %s maxAzimuth: %d, maxAltitude: %d\n",
context_.get(),
__func__,
max_azimuth_,
max_altitude_);
}
void GHOST_Wintab::processInfoChange(LPARAM lParam)
{
/* Update number of connected Wintab digitizers. */
if (LOWORD(lParam) == WTI_INTERFACE && HIWORD(lParam) == IFC_NDEVICES) {
fp_info_(WTI_INTERFACE, IFC_NDEVICES, &num_devices_);
WINTAB_PRINTF("HCTX %p %s numDevices: %d\n", context_.get(), __func__, num_devices_);
}
}
bool GHOST_Wintab::devicesPresent()
{
return num_devices_ > 0;
}
GHOST_TabletData GHOST_Wintab::getLastTabletData()
{
return last_tablet_data_;
}
void GHOST_Wintab::getInput(std::vector<GHOST_WintabInfoWin32> &outWintabInfo)
{
const int numPackets = fp_packets_get_(context_.get(), pkts_.size(), pkts_.data());
outWintabInfo.reserve(numPackets);
for (int i = 0; i < numPackets; i++) {
const PACKET pkt = pkts_[i];
GHOST_WintabInfoWin32 out;
/* % 3 for multiple devices ("DualTrack"). */
switch (pkt.pkCursor % 3) {
case 0:
/* Puck - processed as mouse. */
out.tabletData.Active = GHOST_kTabletModeNone;
break;
case 1:
out.tabletData.Active = GHOST_kTabletModeStylus;
break;
case 2:
out.tabletData.Active = GHOST_kTabletModeEraser;
break;
}
out.x = pkt.pkX;
out.y = pkt.pkY;
if (max_pressure_ > 0) {
out.tabletData.Pressure = float(pkt.pkNormalPressure) / float(max_pressure_);
}
if ((max_azimuth_ > 0) && (max_altitude_ > 0)) {
/* From the wintab spec:
* orAzimuth: Specifies the clockwise rotation of the cursor about the z axis through a
* full circular range.
* orAltitude: Specifies the angle with the x-y plane through a signed, semicircular range.
* Positive values specify an angle upward toward the positive z axis; negative values
* specify an angle downward toward the negative z axis.
*
* wintab.h defines orAltitude as a `uint` but documents orAltitude as positive for upward
* angles and negative for downward angles. WACOM uses negative altitude values to show that
* the pen is inverted; therefore we cast orAltitude as an `int` and then use the absolute
* value.
*/
ORIENTATION ort = pkt.pkOrientation;
/* Convert raw fixed point data to radians. */
float altRad = float((fabs(float(ort.orAltitude)) / float(max_altitude_)) * M_PI_2);
float azmRad = float((float(ort.orAzimuth) / float(max_azimuth_)) * M_PI * 2.0);
/* Find length of the stylus' projected vector on the XY plane. */
float vecLen = cos(altRad);
/* From there calculate X and Y components based on azimuth. */
/* Blender expects: -1.0f (left) to +1.0f (right). */
out.tabletData.Xtilt = sin(azmRad) * vecLen;
/* Blender expects: -1.0f (away from user) to +1.0f (toward user). */
out.tabletData.Ytilt = -float(sin(M_PI_2 - azmRad) * vecLen);
}
out.time = pkt.pkTime;
/* Some Wintab libraries don't handle relative button input, so we track button presses
* manually. */
DWORD buttonsChanged = buttons_ ^ pkt.pkButtons;
/* We only needed the prior button state to compare to current, so we can overwrite it now. */
buttons_ = pkt.pkButtons;
/* Iterate over button flag indices until all flags are clear. */
for (WORD buttonIndex = 0; buttonsChanged; buttonIndex++, buttonsChanged >>= 1) {
if (buttonsChanged & 1) {
GHOST_TButton button = mapWintabToGhostButton(pkt.pkCursor, buttonIndex);
if (button != GHOST_kButtonMaskNone) {
/* If this is not the first button found, push info for the prior Wintab button. */
if (out.button != GHOST_kButtonMaskNone) {
outWintabInfo.push_back(out);
}
out.button = button;
DWORD buttonFlag = 1 << buttonIndex;
out.type = pkt.pkButtons & buttonFlag ? GHOST_kEventButtonDown : GHOST_kEventButtonUp;
}
}
}
outWintabInfo.push_back(out);
}
if (!outWintabInfo.empty()) {
last_tablet_data_ = outWintabInfo.back().tabletData;
}
}
GHOST_TButton GHOST_Wintab::mapWintabToGhostButton(uint cursor, WORD physicalButton)
{
const WORD numButtons = 32;
BYTE logicalButtons[numButtons] = {0};
BYTE systemButtons[numButtons] = {0};
if (!fp_info_(WTI_CURSORS + cursor, CSR_BUTTONMAP, &logicalButtons) ||
!fp_info_(WTI_CURSORS + cursor, CSR_SYSBTNMAP, &systemButtons))
{
return GHOST_kButtonMaskNone;
}
if (physicalButton >= numButtons) {
return GHOST_kButtonMaskNone;
}
BYTE lb = logicalButtons[physicalButton];
if (lb >= numButtons) {
return GHOST_kButtonMaskNone;
}
switch (systemButtons[lb]) {
case SBN_LCLICK:
return GHOST_kButtonMaskLeft;
case SBN_RCLICK:
return GHOST_kButtonMaskRight;
case SBN_MCLICK:
return GHOST_kButtonMaskMiddle;
default:
return GHOST_kButtonMaskNone;
}
}
void GHOST_Wintab::mapWintabToSysCoordinates(int x_in, int y_in, int &x_out, int &y_out)
{
/* Maps from range [in.org, in.org + abs(in.ext)] to [out.org, out.org + abs(out.ext)], in
* reverse if in.ext and out.ext have differing sign. */
auto remap = [](int inPoint, Range in, Range out) -> int {
int absInExt = abs(in.ext);
int absOutExt = abs(out.ext);
/* Translate input from range [in.org, in.org + absInExt] to [0, absInExt] */
int inMagnitude = inPoint - in.org;
/* If signs of extents differ, reverse input over range. */
if ((in.ext < 0) != (out.ext < 0)) {
inMagnitude = absInExt - inMagnitude;
}
/* Scale from [0, absInExt] to [0, absOutExt]. */
int outMagnitude = inMagnitude * absOutExt / absInExt;
/* Translate from range [0, absOutExt] to [out.org, out.org + absOutExt]. */
int outPoint = outMagnitude + out.org;
return outPoint;
};
x_out = remap(x_in, tablet_coord_.x, system_coord_.x);
y_out = remap(y_in, tablet_coord_.y, system_coord_.y);
}
bool GHOST_Wintab::trustCoordinates()
{
return coord_trusted_;
}
bool GHOST_Wintab::testCoordinates(int sysX, int sysY, int wtX, int wtY)
{
mapWintabToSysCoordinates(wtX, wtY, wtX, wtY);
/* Allow off by one pixel tolerance in case of rounding error. */
if (abs(sysX - wtX) <= 1 && abs(sysY - wtY) <= 1) {
coord_trusted_ = true;
return true;
}
else {
coord_trusted_ = false;
return false;
}
}
bool GHOST_Wintab::debug_ = false;
void GHOST_Wintab::setDebug(bool debug)
{
debug_ = debug;
}
bool GHOST_Wintab::getDebug()
{
return debug_;
}
void GHOST_Wintab::printContextDebugInfo()
{
if (!debug_) {
return;
}
/* Print button maps. */
BYTE logicalButtons[32] = {0};
BYTE systemButtons[32] = {0};
for (int i = 0; i < 3; i++) {
printf("initializeWintab cursor %d buttons\n", i);
uint lbut = fp_info_(WTI_CURSORS + i, CSR_BUTTONMAP, &logicalButtons);
if (lbut) {
printf("%d", logicalButtons[0]);
for (int j = 1; j < lbut; j++) {
printf(", %d", logicalButtons[j]);
}
printf("\n");
}
else {
printf("logical button error\n");
}
uint sbut = fp_info_(WTI_CURSORS + i, CSR_SYSBTNMAP, &systemButtons);
if (sbut) {
printf("%d", systemButtons[0]);
for (int j = 1; j < sbut; j++) {
printf(", %d", systemButtons[j]);
}
printf("\n");
}
else {
printf("system button error\n");
}
}
/* Print context information. */
/* Print open context constraints. */
uint maxcontexts, opencontexts;
fp_info_(WTI_INTERFACE, IFC_NCONTEXTS, &maxcontexts);
fp_info_(WTI_STATUS, STA_CONTEXTS, &opencontexts);
printf("%u max contexts, %u open contexts\n", maxcontexts, opencontexts);
/* Print system information. */
printf("left: %d, top: %d, width: %d, height: %d\n",
::GetSystemMetrics(SM_XVIRTUALSCREEN),
::GetSystemMetrics(SM_YVIRTUALSCREEN),
::GetSystemMetrics(SM_CXVIRTUALSCREEN),
::GetSystemMetrics(SM_CYVIRTUALSCREEN));
auto printContextRanges = [](LOGCONTEXT &lc) {
printf("lcInOrgX: %d, lcInOrgY: %d, lcInExtX: %d, lcInExtY: %d\n",
lc.lcInOrgX,
lc.lcInOrgY,
lc.lcInExtX,
lc.lcInExtY);
printf("lcOutOrgX: %d, lcOutOrgY: %d, lcOutExtX: %d, lcOutExtY: %d\n",
lc.lcOutOrgX,
lc.lcOutOrgY,
lc.lcOutExtX,
lc.lcOutExtY);
printf("lcSysOrgX: %d, lcSysOrgY: %d, lcSysExtX: %d, lcSysExtY: %d\n",
lc.lcSysOrgX,
lc.lcSysOrgY,
lc.lcSysExtX,
lc.lcSysExtY);
};
LOGCONTEXT lc;
/* Print system context. */
fp_info_(WTI_DEFSYSCTX, 0, &lc);
printf("WTI_DEFSYSCTX\n");
printContextRanges(lc);
/* Print system context, manually populated. */
fp_info_(WTI_DEFSYSCTX, CTX_INORGX, &lc.lcInOrgX);
fp_info_(WTI_DEFSYSCTX, CTX_INORGY, &lc.lcInOrgY);
fp_info_(WTI_DEFSYSCTX, CTX_INEXTX, &lc.lcInExtX);
fp_info_(WTI_DEFSYSCTX, CTX_INEXTY, &lc.lcInExtY);
fp_info_(WTI_DEFSYSCTX, CTX_OUTORGX, &lc.lcOutOrgX);
fp_info_(WTI_DEFSYSCTX, CTX_OUTORGY, &lc.lcOutOrgY);
fp_info_(WTI_DEFSYSCTX, CTX_OUTEXTX, &lc.lcOutExtX);
fp_info_(WTI_DEFSYSCTX, CTX_OUTEXTY, &lc.lcOutExtY);
fp_info_(WTI_DEFSYSCTX, CTX_SYSORGX, &lc.lcSysOrgX);
fp_info_(WTI_DEFSYSCTX, CTX_SYSORGY, &lc.lcSysOrgY);
fp_info_(WTI_DEFSYSCTX, CTX_SYSEXTX, &lc.lcSysExtX);
fp_info_(WTI_DEFSYSCTX, CTX_SYSEXTY, &lc.lcSysExtY);
printf("WTI_DEFSYSCTX CTX_*\n");
printContextRanges(lc);
for (uint i = 0; i < num_devices_; i++) {
/* Print individual device system context. */
fp_info_(WTI_DSCTXS + i, 0, &lc);
printf("WTI_DSCTXS %u\n", i);
printContextRanges(lc);
/* Print individual device system context, manually populated. */
fp_info_(WTI_DSCTXS + i, CTX_INORGX, &lc.lcInOrgX);
fp_info_(WTI_DSCTXS + i, CTX_INORGY, &lc.lcInOrgY);
fp_info_(WTI_DSCTXS + i, CTX_INEXTX, &lc.lcInExtX);
fp_info_(WTI_DSCTXS + i, CTX_INEXTY, &lc.lcInExtY);
fp_info_(WTI_DSCTXS + i, CTX_OUTORGX, &lc.lcOutOrgX);
fp_info_(WTI_DSCTXS + i, CTX_OUTORGY, &lc.lcOutOrgY);
fp_info_(WTI_DSCTXS + i, CTX_OUTEXTX, &lc.lcOutExtX);
fp_info_(WTI_DSCTXS + i, CTX_OUTEXTY, &lc.lcOutExtY);
fp_info_(WTI_DSCTXS + i, CTX_SYSORGX, &lc.lcSysOrgX);
fp_info_(WTI_DSCTXS + i, CTX_SYSORGY, &lc.lcSysOrgY);
fp_info_(WTI_DSCTXS + i, CTX_SYSEXTX, &lc.lcSysExtX);
fp_info_(WTI_DSCTXS + i, CTX_SYSEXTY, &lc.lcSysExtY);
printf("WTI_DSCTX %u CTX_*\n", i);
printContextRanges(lc);
/* Print device axis. */
AXIS axis_x, axis_y;
fp_info_(WTI_DEVICES + i, DVC_X, &axis_x);
fp_info_(WTI_DEVICES + i, DVC_Y, &axis_y);
printf("WTI_DEVICES %u axis_x org: %d, axis_y org: %d axis_x ext: %d, axis_y ext: %d\n",
i,
axis_x.axMin,
axis_y.axMin,
axis_x.axMax - axis_x.axMin + 1,
axis_y.axMax - axis_y.axMin + 1);
}
/* Other stuff while we have a log-context. */
printf("sysmode %d\n", lc.lcSysMode);
}