diff --git a/libctru/include/3ds.h b/libctru/include/3ds.h index 141c6ccc5..9d7d28ce2 100644 --- a/libctru/include/3ds.h +++ b/libctru/include/3ds.h @@ -75,6 +75,7 @@ extern "C" { #include <3ds/services/nfc.h> #include <3ds/services/news.h> #include <3ds/services/qtm.h> +#include <3ds/services/qtmc.h> #include <3ds/services/srvpm.h> #include <3ds/services/loader.h> #include <3ds/services/y2r.h> diff --git a/libctru/include/3ds/services/qtm.h b/libctru/include/3ds/services/qtm.h index 363fc2f25..e9fdb73fc 100644 --- a/libctru/include/3ds/services/qtm.h +++ b/libctru/include/3ds/services/qtm.h @@ -1,57 +1,559 @@ /** * @file qtm.h - * @brief QTM service. + * @brief QTM services. + * + * QTM is responsible for the following: + * - tracking and predicting the position of the user's eyes. This is done by using the inner + * camera sampling at 320x240px at 30 FPS, and by using the gyroscope to predict the position + * of the eyes between two camera samples (effectively doubling the tracking rate). + * The API reporting eye tracking data is actually *console-agnostic*. This concept is most + * likely covered by patent US9098112B2 + * - automatically managing the IR LED emitter used for eye tracking in low-light conditions + * - managing the state of the parallax barrier by controlling the positions of the barrier's + * mask (opaque/transparent state by repeating pattern of 12 units, plus polarity). This is + * done via a TI TCA6416A I2C->Parallel expander (highlighted in yellow in this teardown photo: + * https://guide-images.cdn.ifixit.com/igi/IKlW6WTZKKmapYkt.full, with the expected 12 traces + * being clearly visible near the ribbon cable slot) + * - updating the barrier position according to eye tracking data relative to an optimal setting + * stored in calibration data: this is what Nintendo calls "Super Stable 3D" (SS3D); not done + * when in 2D mode + * + * Head-tracking can be used even if SS3D is disabled. + * + * SS3D works as follows: + * - compute the raw X and Y pixel coordinates for the eye midpoint: + * `rawX = (leftEyeRawPxCoords.x + rightEyeRawPxCoords.x) / 2` + * `rawY = (leftEyeRawPxCoords.y + rightEyeRawPxCoords.y) / 2` + * - rotate the value around the optical Z-axis using the optimal eye-to-camera angle from + * calibration data, with a rotation matrix + * - normalize the X value: + * `xC = (rawX / 320) - 0.5` + * - transform into world space coordinate, using fovX from calibration (convert to radians first). + * Note that this fovX doesn't take lens distortion into account and is slightly different from + * the hardcoded angle used in \ref QTMU_GetTrackingData + * `x = xC * tan(fovX/2)` + * - multiply by length of adjacent side (eye-to-camera distance) to get the length of the opposite + * side. This is the eye horizontal deviation to camera lens in mm, which is then converted to + * number of iod/12 units: + * `delta = x * optimalDistance / (iod/12)` + * - we then obtain the new target position of the parallax barrier (expressed in iod/12 units, + * mod iod/12): + * `pos = centerPos + delta` + * - the value is then rounded to nearest integer. To avoid artifacts, if the rounded value is + * going to increase, then 0.01 is subtracted, and if it is going to decrease, then 0.01 is added. + * The value is rounded again to compute the final discrete value written to the expander (barrier + * position). + * - note: all calculation in QTM and otherwise assume interocular distance to be 62mm (the average). + * Assumedly, if the user's IOD is different, then "optimal distance to screen" effectively changes + * for that user. + * + * QTM services are not present on O3DS, thus caller must call \ref qtmCheckServicesRegistered to check + * if the services are registered. Moreover, since QTM functionality might not always be available (due + * to blacklist or console being N2DSXL), `qtm:u` users should check Result return values, and `qtm:s` + * users can call \ref QTMS_SetQtmStatus to check the actual availability status. + * + * Considering that the eye tracking data reporting API is hardware-agnostic, it is advisable to + * hardcode neither camera aspect ratio (even if it is 4/3 on real hardware) and resolution nor + * field-of-view angle. + * + * There is a separate QTM service, `qtm:c` ("hardware check"), that lets you manipulate parallax barrier + * pattern directly. It is covered in \ref qtmc.h instead. */ #pragma once -//See also: http://3dbrew.org/wiki/QTM_Services +#include <3ds/types.h> -/// Head tracking coordinate pair. -typedef struct { - float x; ///< X coordinate. - float y; ///< Y coordinate. -} QTM_HeadTrackingInfoCoord; +/// ID of QTM status data (fully enabled/SS3D disabled) in `cfg` +#define QTM_STATUS_CFG_BLK_ID 0x180000u -/// Head tracking info. -typedef struct { - u8 flags[5]; ///< Flags. - u8 padding[3]; ///< Padding. - float floatdata_x08; ///< Unknown. Not used by System_Settings. - QTM_HeadTrackingInfoCoord coords0[4]; ///< Head coordinates. - u32 unk_x2c[5]; ///< Unknown. Not used by System_Settings. -} QTM_HeadTrackingInfo; +/// ID of QTM calibration data in `cfg` +#define QTM_CAL_CFG_BLK_ID 0x180001u -/// Initializes QTM. -Result qtmInit(void); +/** + * @brief QTM enablement status (when cameras not in use by user), set by `qtm:s`. + * @note Manual IR LED control, camera lux, and `qtm:c` commands remain available + * for use on N3DS and N3DSXL regardless. + */ +typedef enum QtmStatus { + QTM_STATUS_ENABLED = 0, ///< QTM is fully enabled. + QTM_STATUS_SS3D_DISABLED = 1, ///< QTM "super stable 3D" feature is disabled. Parallax barrier hardware state is configured to match O3DS. + + /** + * @brief QTM is unavailable: either "blacklisted" (usually by NS) for the current title, **or console is a N2DSXL**. + * + * In this state, all QTM functionality is disabled. This includes "super-stable 3D" + * (ie. auto barrier adjustment) including `qtm:s` manual barrier position setting functions, + * head tracking, IR LED control and camera luminance reporting (400.0 is returned instead). + * + * @note `qtm:c` barrier hardware state setting function (\ref blah) bypasses this state. + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. + */ + QTM_STATUS_UNAVAILABLE = 2, +} QtmStatus; + +/// QTM status data (fully enabled/SS3D disabled) in `cfg`. Usually all-zero on N2DSXL. +typedef struct QtmStatusCfgData { + QtmStatus defaultStats; ///< QTM status at boot (fully enabled or SS3D disabled). + /** + * @brief "Global variable" (.data) section load mode? Unused. + * From CTRAging: + * - 0: "normal" + * - 1: "single reacq" + * - 2: "double reacq" + - 3/4/5: "w2w copy 1/10/100" + */ + u8 gvLoadMode; + u8 _padding[2]; ///< Padding. +} QtmStatusCfgData; + +/// QTM calibration data (fully enabled/SS3D disabled) in `cfg`. Usually all-zero on N2DSXL. +typedef struct QtmCalibrationData { + /** + * @brief Neutral (center) barrier position/offset (with slit width of 6 units), when the user is + * facing directly facing the camera, that is to say, their eye midpoint normalized X coord + * in the camera's plane is 0, assuming the user's head is located at the expected viewing distance + * and at the expected eye-to-camera angle (as per the rest of this structure). + * This is expressed in terms of iod/12 units modulo iod/12 (thus, range is 0 to 11 included), + * with IOD (interocular distance) assumed to be 62mm. + * @note This field is floating-point for QTM auto-adjustment purposes, however the actual barrier + * position in hardware is an integer. + * @note This is the field that System Settings lets you add -1.0 to +1.0 to. + * @note Moreover, this field can be directly changed through \ref QTMS_SetCenterBarrierPosition. + */ + float centerBarrierPosition; + + float translationX; ///< Lens X coord in inner camera space? Very low value and seems to be unused. + float translationY; ///< Lens Y coord in inner camera space? Very low value and seems to be unused. + float rotationZ; ///< Optimal eye-to-camera angle, in radians, without accounting for lens distortion. + float fovX; ///< Camera's horizontal FoV in degrees, without accounting for lens distortion. + float viewingDistance; ///< Optimal viewing distance between user and top screen, assuming iod to be 62mm. +} QtmCalibrationData; + +/// Left eye or right eye, for \ref QtmTrackingData and \ref QtmRawTrackingData +typedef enum QtmEyeSide { + QTM_EYE_LEFT = 0, ///< Left eye. + QTM_EYE_RIGHT = 1, ///< Right eye. + + QTM_EYE_NUM, ///< Number of eyes. +} QtmEyeSide; + +/// QTM raw eye tracking data +typedef struct QtmRawTrackingData { + /** + * @brief Eye position detected or predicted, equals (confidenceLevel > 0). + * If false, QTM will attempt to make a guess based on gyro data. + * If the console isn't moving either, then QTM will assume the user's eyes are progressively + * moving back to face the screen. + */ + bool eyesTracked; ///< Eye position detected or predicted, equals (confidenceLevel > 0). + u8 _padding[3]; ///< Padding. + u32 singletonQtmPtr; ///< Pointer to eye-tracking singleton pointer, in QTM's .bss, located in N3DS extra memory. + float confidenceLevel; ///< Eye tracking confidence level (0 to 1). + + /** + * @brief Raw predicted or detected eye coordinates. Each eye is represented as one point. + * Fractional part is *not* necessarily zero. + * @note X coord is within 0 to 320. + * @note Y coord is within 0 to 240. + */ + float rawEyeCameraCoordinates[QTM_EYE_NUM][2]; + + float dPitch; ///< Difference in gyro pitch from position at console boot. + float dYaw; ///< Difference in gyro yaw from position at console boot. + float dRoll; ///< Difference in gyro roll from position at console boot. + + s64 samplingTick; ///< Time point the current measurements were made. +} QtmRawTrackingData; + +/// QTM processed eye tracking data, suitable for 3D programming +typedef struct QtmTrackingData { + bool eyesTracked; ///< Eye position detected or tracked with some confidence, equals (confidenceLevel > 0). Even if false, QTM may make a guess + bool faceDetected; ///< Whether or not the entirety of the user's face has been detected with good confidence. + bool eyesDetected; ///< Whether or not the user's eyes have actually been detected with full confidence. + u8 _unused; ///< Unused. + bool clamped; ///< Whether or not the normalized eye coordinates have been clamped after accounting for lens distortion. + u8 _padding[3]; ///< Padding. + + float confidenceLevel; ///< Eye tracking confidence level (0 to 1). + + /** + * @brief Normalized eye coordinates, for each eye, after accounting for lens distortion, centered around camera. + * X coord is in the -1 to 1 range, and Y coord range depends on inverse aspect ratio (-0.75 to 0.75 on real hardware). + * @note On real hardware, X coord equals `((rawX / 160.0) - 1.00) * 1.0639` before clamping. + * @note On real hardware, Y coord equals `((rawY / 160.0) - 0.75) * 1.0637` before clamping. + */ + float eyeCameraCoordinates[QTM_EYE_NUM][2]; + + /** + * @brief Normalized eye coordinates, for each eye, in world space. + * Corresponds to \ref eyeCameraCoordinates multiplied by tangent of field of view. + * @note On real hardware, X coord equals `eyeCameraCoordinates.x * tan(64.9 deg / 2)`. + * @note On real hardware, Y coord equals `eyeCameraCoordinates.x * tan(51.0 deg / 2)`. + */ + float eyeWorldCoordinates[QTM_EYE_NUM][2]; + + float dPitch; ///< Difference in gyro pitch from position at console boot. + float dYaw; ///< Difference in gyro yaw from position at console boot. + float dRoll; ///< Difference in gyro roll from position at console boot. + + + s64 samplingTick; ///< Time point the current measurements were made. +} QtmTrackingData; + +/// QTM service name enum, excluding `qtm:c` +typedef enum QtmServiceName { + /** + * @brief `qtm:u`: has eye-tracking commands and IR LED control commands, but for some + * reason cannot fetch ambiant lux data from the camera's luminosity sensor. + */ + QTM_SERVICE_USER = 0, + + /** + * @brief `qtm:s`: has access to all `qtm:u` commands, plus luminosity sensor, plus + * manual barrier position setting and calibration adjustment commands. + * Automatic barrier control is reenabled on session exit. + */ + QTM_SERVICE_SYSTEM = 1, + + /** + * @brief `qtm:sp`: has access to all `qtm:s` (and `qtm:u`) commands, and merely has a + * few more commands that GSP uses to notify QTM of 2D<>3D mode switches and + * power events. Automatic barrier control is reenabled on session exit. + * GSP always keeps a `qtm:sp` sessions open (at least on latest system version), + * whereas NS opens then immediately closes a `qtm:sp` sessions only when dealing + * with a "blacklisted" application (that is, almost never). + */ + QTM_SERVICE_SYSTEM_PROCESS = 2, +} QtmServiceName; + +/** + * @brief Check whether or not QTM services are registered. + * @return True on O3DS systems, false on N3DS systems. + */ +bool qtmCheckServicesRegistered(void); + +/** + * @brief Initializes QTM (except `qtm:c`). + * Excluding `qtm:c`, QTM has three main services. + * Only 3 sessions (2 until 9.3.0 sysupdate) for ALL services COMBINED, including `qtm:c`, + * can be open at a time. + * Refer to \ref QtmServiceName enum value descriptions to see which service to choose. + * + * @param serviceName QTM service name enum value (corresponding to `qtm:u`, `qtm:s` and `qtm:sp` + * respectively). + * @note Result of \ref qtmCheckServicesRegistered should be checked before calling this function. + */ +Result qtmInit(QtmServiceName serviceName); /// Exits QTM. void qtmExit(void); +/// Checks whether or not a `qtm:u`, `qtm:s` or `qtm:sp` session is active. +bool qtmIsInitialized(void); + +/// Returns a pointer to the current `qtm:u` / `qtm:s` / `qtm:sp` session handle. +Handle *qtmGetSessionHandle(void); + +/** + * @brief Gets the current raw eye tracking data, with an optional prediction made for predictionTimePointOrZero = t+dt, + * or for the current time point (QTM makes predictions based on gyro data since inner camera runs at 30 FPS). + * + * @param[out] outData Where to write the raw tracking data to. Cleared to all-zero on failure (instead of being left uninitialized). + * @param predictionTimePointOrZero Either zero, or the time point (in system ticks) for which to make a prediction for. + * Maximum 1 frame (at 30 FPS) in the past, and up to 5 frames in the future. + * @return `0xC8A18008` if camera is in use by user, or `0xC8A183EF` if QTM is unavailable (in particular, QTM is always + * unavailable on N2DSXL), Otherwise, 0 (success). Return value should be checked by caller. + * @note Consider using \ref QTMU_GetTrackingDataEx instead. + */ +Result QTMU_GetRawTrackingDataEx(QtmRawTrackingData *outData, s64 predictionTimePointOrZero); + +/** + * @brief Gets the current raw eye tracking data. + * + * @param[out] outData Where to write the raw tracking data to. Cleared to all-zero on failure (instead of being left uninitialized). + * @return `0xC8A18008` if camera is in use by user, or `0xC8A183EF` if QTM is unavailable (in particular, QTM is always + * unavailable on N2DSXL), Otherwise, 0 (success). Return value should be checked by caller. + * @note Consider using \ref QTMU_GetTrackingData instead. + */ +static inline Result QTMU_GetRawTrackingData(QtmRawTrackingData *outData) +{ + return QTMU_GetRawTrackingDataEx(outData, 0LL); +} + +/** + * @brief Gets the current normalized eye tracking data, made suitable for 3D programming with an optional prediction made + * for predictionTimePointOrZero = t+dt, or for the current time point (QTM makes predictions based on gyro data since + * inner camera runs at 30 FPS). + * + * @param[out] outData Where to write the raw tracking data to. Cleared to all-zero on failure (instead of being left uninitialized). + * @param predictionTimePointOrZero Either zero, or the time point (in system ticks) for which to make a prediction for. + * Maximum 1 frame (at 30 FPS) in the past, and up to 5 frames in the future. + * @return `0xC8A18008` if camera is in use by user, or `0xC8A183EF` if QTM is unavailable (in particular, QTM is always + * unavailable on N2DSXL). Otherwise, 0 (success). Return value should be checked by caller. + * @note This can, for example, be used in games to allow the user to control the scene's camera with their own face. + */ +Result QTMU_GetTrackingDataEx(QtmTrackingData *outData, s64 predictionTimePointOrZero); + +/** + * @brief Gets the current normalized eye tracking data, made suitable for 3D programming. + * + * @param[out] outData Where to write the raw tracking data to. Cleared to all-zero on failure (instead of being left uninitialized). + * @return `0xC8A18008` if camera is in use by user, or `0xC8A183EF` if QTM is unavailable (in particular, QTM is always + * unavailable on N2DSXL). Otherwise, 0 (success). Return value should be checked by caller. + * @note This can, for example, be used in games to allow the user to control the scene's camera with their own face. + */ +static inline Result QTMU_GetTrackingData(QtmTrackingData *outData) +{ + return QTMU_GetTrackingDataEx(outData, 0LL); +} + +/** + * @brief Computes an approximation of the horizontal angular field of view of the camera based on eye tracking data. + * + * @param data Eye tracking data, obtained from \ref QTMU_GetTrackingData or \ref QTMU_GetTrackingDataEx. + * @return Horizontal angular field of view in radians. Corresponds to 64.9 degrees on real hardware. + */ +float qtmComputeFovX(const QtmTrackingData *data); + +/** + * @brief Computes an approximation of the vertical angular field of view of the camera based on eye tracking data. + * + * @param data Eye tracking data, obtained from \ref QTMU_GetTrackingData or \ref QTMU_GetTrackingDataEx. + * @return Vertical angular field of view in radians. Corresponds to 51.0 degrees on real hardware. + */ +float qtmComputeFovY(const QtmTrackingData *data); + +/** + * @brief Computes a rough approximation of the inverse of the aspect ration of the camera based on eye tracking data. + * + * @param data Eye tracking data, obtained from \ref QTMU_GetTrackingData or \ref QTMU_GetTrackingDataEx. + * @return Rough approximation of the inverse of the aspect ratio of the camera. Aspect ratio is exactly 0.75 on real hardware. + */ +float qtmComputeInverseAspectRatio(const QtmTrackingData *data); + +/** + * @brief Computes the user's head tilt angle, that is, the angle between the line through both eyes and the camera's + * horizontal axis in camera space. + * + * @param data Eye tracking data, obtained from \ref QTMU_GetTrackingData or \ref QTMU_GetTrackingDataEx. + * @return Horizontal head angle relative to camera, in radians. + */ +float qtmComputeHeadTiltAngle(const QtmTrackingData *data); + +/** + * @brief Estimates the distance between the user's eyes and the camera, based on + * eye tracking data. This may be a little bit inaccurate, as this assumes + * interocular distance of 62mm (like all 3DS software does), and that both + * eyes are at the same distance from the screen. + * + * @param data Eye tracking data, obtained from \ref QTMU_GetTrackingData or \ref QTMU_GetTrackingDataEx. + * @return Eye-to-camera distance in millimeters. + */ +float qtmEstimateEyeToCameraDistance(const QtmTrackingData *data); + +/** + * @brief Temporarily enables manual control of the IR LED by user, disabling its automatic control. + * If not already done, this also turns off the IR LED. This setting is cleared when user closes the console's shell. + * @return Always 0 (success). + */ +Result QTMU_EnableManualIrLedControl(void); + +/** + * @brief Temporarily disables manual control of the IR LED by user, re-enabling its automatic control. + * If not already done, this also turns off the IR LED. + * @return Always 0 (success). + */ +Result QTMU_DisableManualIrLedControl(void); + +/** + * @brief Turns the IR LED on or off during manual control. \ref QTMU_EnableManualIrLedControl must have been called. + * + * @param on Whether to turn the IR LED on or off. + * @return `0xC8A18005` if manual control was not enabled or if the operation failed, `0xC8A18008` if camera is in use + * by user, or `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL). + * Otherwise, 0 (success). + */ +Result QTMU_SetIrLedStatus(bool on); + +/** + * @brief Attempts to clear IR LED overrides from any of the relevant commands in `qtm:u`, `qtm:s` (and `qtm:c`) commands + * by calling \ref QTMU_EnableManualIrLedControl followed by \ref QTMU_DisableManualIrLedControl, so that auto IR LED + * management takes place again. + * @return The value returned by \ref QTMU_DisableManualIrLedControl. + */ +Result qtmClearIrLedOverrides(void); + +/** + * @brief Checks whether or not QTM has been blacklisted, ie. that it has been made unavailable. + * In detail, this means that the last call to \ref QTMS_SetQtmStatus was made with argument \ref QTM_STATUS_UNAVAILABLE, + * usually by NS. This feature seems to only be used for some internal test titles. + * + * @param[out] outBlacklisted Whether or not QTM is unavailable. Always true on N2DSXL. + * @return Always 0 (success). + * @note On N2DSXL, even though status is always supposed to be \ref QTM_STATUS_UNAVAILABLE, this function often returns true + * (because NS doesn't change QTM's status if title isn't blacklisted). Do not rely on this for N2DSXL detection. + * @note Refer to https://www.3dbrew.org/wiki/NS_CFA for a list of title UIDs this is used for. + */ +Result QTMU_IsCurrentAppBlacklisted(bool *outBlacklisted); + +/** + * @brief Sets the neutral (center) barrier position/offset in calibration, _without_ saving it to `cfg`. + * Takes effect immediately. SS3D works by calculating the position of the eye midpoint, rotated + * by the ideal eye-to-camera angle, expressed in (iod/12 units, iod assumed to be 62mm). + * + * @param position Center barrier position, in terms of iod/12 units modulo iod/12. + * @note This field is floating-point for QTM auto-adjustment purposes, however the actual barrier position + * in hardware is an integer. + * @note This is the field that System Settings lets you add -1.0 to +1.0 to. + * @note There is no "get" counterpart for this. + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), otherwise + 0 (success). + */ +Result QTMS_SetCenterBarrierPosition(float position); + +/** + * @brief Gets the average ambient luminance as perceived by the inner camera (in lux). + * If QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), returns 400.0 instead + * of the actual luminance. + * + * @param[out] outLuminanceLux Where to write the luminance to. Always 400.0 on N2DSXL. + * @note Camera exposure, and in particular auto-exposure affects the returned luminance value. This must be + * taken into consideration, because this value can thus surge when user covers the inner camera. + * @return Always 0 (success). + */ +Result QTMS_GetCameraLuminance(float *outLuminanceLux); + +/** + * @brief Enables automatic barrier control when in 3D mode with "super stable 3D" enabled. + * + * @note This is automatically called upon `qtm:s` and `qtm:sp` session exit. + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), otherwise + 0 (success). + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. If that is regardless the case, + * this function here does nothing. + */ +Result QTMS_EnableAutoBarrierControl(void); + +/** + * @brief Temporarily disables automatic barrier control (when in 3D mode with "super stable 3D" enabled). + * + * @note This is automatically called upon `qtm:s` and `qtm:sp` session exit. + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), otherwise + 0 (success). + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. If that is regardless the case, + * this function here does nothing. + */ +Result QTMS_DisableAutoBarrierControl(void); + +/** + * @brief Temporarily sets the parallax barrier's position (offset in iod/12 units, assuming slit width of 6 units). + * Does nothing in 2D mode and/or if "super stable 3D" is disabled. + * + * @param position Parallax barrier position (offset in units), must be between 0 and 11 (both included) + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), 0xE0E18002 + * if \p position is not in range, otherwise 0 (success). + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. If that is regardless the case, + * this function here does nothing. + * @note No effect when the screen is in 2D mode. + * @see QTMC_SetBarrierPattern + */ +Result QTMS_SetBarrierPosition(u8 position); + +/** + * @brief Gets the current position of the parallax barrier (offset in iod/12 units, slit width of 6 units). + * When "super stable 3D" is disabled, returns 13 instead. + * + * @param[out] outPosition Where to write the barrier's position to. + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), otherwise + 0 (success). + * @note When SS3D is disabled, this returns 13 to \p outPosition . When in 2D mode, the returned position is not + updated. + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. If that is regardless the case, + * this function here returns 13 to \p outPosition . + * @see QTMC_SetBarrierPattern + */ +Result QTMS_GetCurrentBarrierPosition(u8 *outPosition); + +/** + * @brief Temporarily overrides IR LED state. Requires "manual control" from `qtm:u` to be disabled, and has + * lower priority than it. + * + * @param on Whether to turn the IR LED on or off. + * @return `0xC8A18005` if manual control was enabled or if the operation failed, `0xC8A18008` if camera is in use + * by user (unless "hardware check" API enabled), or `0xC8A18009` if QTM is unavailable (in particular, + * QTM is always unavailable on N2DSXL). Otherwise, 0 (success). + */ +Result QTMS_SetIrLedStatusOverride(bool on); + +/** + * @brief Sets calibration data, taking effect immediately, and optionally saves it to `cfg`. + * + * @param cal Pointer to calibration data. + * @param saveCalToCfg Whether or not to persist the calibration data in `cfg`. + * @return `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL), otherwise + whatever `cfg:s` commands return (if used), or 0 (success). + * @note There is no "get" counterpart for this function, and there is no way to see the current calibration data + in use unless it has been saved to `cfg`. + * @note Due to an oversight, \ref QTMS_SetQtmStatus allows changing QTM state on N2DSXL. This is not intended + * to be done, and is in fact never done by official software. If that is regardless the case, + * this function here doesn't apply calibrations parameters (they may still be saved, however, + * even though QTM calibration blocks are always normally 0 on N2DSXL). + */ +Result QTMS_SetCalibrationData(const QtmCalibrationData *cal, bool saveCalToCfg); + +/** + * @brief Gets the current QTM status (enabled/ss3d disabled/unavailable). + * + * @param[out] outQtmStatus Where to write the QTM status to. + * @return Always 0. + */ +Result QTMS_GetQtmStatus(QtmStatus *outQtmStatus); + +/** + * @brief Gets the current QTM status (enabled/ss3d disabled/unavailable). Also sets or clear the + * "blacklisted" flag returned by \ref QTMU_IsCurrentAppBlacklisted. + * + * @param qtmStatus QTM status to set. If equal to \ref QTM_STATUS_UNAVAILABLE, sets the "blacklisted" flag, + * otherwise clears it. + * @return `0xE0E18002` if enum value is invalid, otherwise 0 (success). + * @note System settings uses this to disable super-stable 3D, and NS to "blacklist" (make QTM unavailable) + * specific applications. + */ +Result QTMS_SetQtmStatus(QtmStatus qtmStatus); + /** - * @brief Checks whether QTM is initialized. - * @return Whether QTM is initialized. + * @brief Called by GSP's LCD driver to signal 2D<>3D mode change + * @param newMode 0 for 2D, 1 for 800px 2D (unused for this function, same as 0), 2 for 3D + * @return Always 0 (success). */ -bool qtmCheckInitialized(void); +Result QTMSP_NotifyTopLcdModeChange(u8 newMode); /** - * @brief Checks whether a head is fully detected. - * @param info Tracking info to check. + * @brief Called by GSP's LCD driver during top LCD power-on to signal to QTM that it may power on + * and/or reconfigure then use the TI TCA6416A expander. In the process, QTM re-creates its + * expander thread. + * @return Always 0 (success). */ -bool qtmCheckHeadFullyDetected(QTM_HeadTrackingInfo *info); +Result QTMSP_NotifyTopLcdPowerOn(void); /** - * @brief Converts QTM coordinates to screen coordinates. - * @param coord Coordinates to convert. - * @param screen_width Width of the screen. Can be NULL to use the default value for the top screen. - * @param screen_height Height of the screen. Can be NULL to use the default value for the top screen. - * @param x Pointer to output the screen X coordinate to. - * @param y Pointer to output the screen Y coordinate to. + * @brief Called by GSP's LCD driver to know whether or not QTM's expander thread is using + * the TI TCA6416A expander; it is waiting for this to become true/false during LCD + * power on/power off to proceed. Always false on N2DSXL. + * @param[out] outActive Where to write the "in use" status to. + * @return Always 0 (success). */ -Result qtmConvertCoordToScreen(QTM_HeadTrackingInfoCoord *coord, float *screen_width, float *screen_height, u32 *x, u32 *y); +Result QTMSP_IsExpanderInUse(bool *outActive); /** - * @brief Gets the current head tracking info. - * @param val Normally 0. - * @param out Pointer to write head tracking info to. + * @brief Called by GSP's LCD driver during top LCD power-on to signal to QTM that it needs to + * switch the parallax barrier state to a 2D state (all-transparent mask). Causes QTM's + * expander thread to exit, relinquishing its `i2c::QTM` session with it. + * @return Always 0 (success). */ -Result QTM_GetHeadTrackingInfo(u64 val, QTM_HeadTrackingInfo* out); +Result QTMSP_NotifyTopLcdPowerOff(void); diff --git a/libctru/include/3ds/services/qtmc.h b/libctru/include/3ds/services/qtmc.h new file mode 100644 index 000000000..4b3ba3527 --- /dev/null +++ b/libctru/include/3ds/services/qtmc.h @@ -0,0 +1,117 @@ +/** + * @file qtmc.h + * @brief QTM Hardware Check service. + * + * Allows direct control over the parallax barrier's pattern and polarity phase through the + * TI TCA6416A I2C->Parallel expander, as well as control over IR LED state even when camera + * is used by user. + * + * TI TCA6416A I2C->Parallel expander is located on bus I2C1 (PA 0x10161000) device ID 0x40. + * + * The top screen parallax barrier was covered by patent US20030234980A1. + */ +#pragma once + +#include <3ds/types.h> + +/** + * @brief Initializes `qtm:c`. + * Only 3 sessions (2 until 9.3.0 sysupdate) for ALL services COMBINED, including the main + * services, can be open at a time. + */ +Result qtmcInit(void); + +/// Exits `qtm:c`. +void qtmcExit(void); + +/// Returns a pointer to the current `qtm:c` session handle. +Handle *qtmcGetSessionHandle(void); + +/** + * @brief Starts the QTM Hardware Check API. This must be called before using any other `qtm:c` command, + * and causes barrier pattern to be overriden by what was last set in \ref QTMC_SetBarrierPattern, + * **even in 2D mode**. Also allows IR LED state to be overridden even if user uses the inner camera. + * @return `0xD82183F9` if already started, otherwise 0 (success). + */ +Result QTMC_StartHardwareCheck(void); + +/** + * @brief Stops the QTM Hardware Check API. Restore normal barrier and IR LED management behavior. + * @return `0xD82183F8` if API not started, otherwise 0 (success). + */ +Result QTMC_StopHardwareCheck(void); + +/** + * @brief Sets the parallax barrier's mask pattern and polarity phase (12+1 bits). + * + * Bit11 to 0 correspond to a repeating barrier mask pattern, 0 meaning the corresponding mask unit is + * transparent and 1 that it is opaque. The direction is: left->right corresponds to MSB->LSB. + * + * Bit12 is the polarity bit. + * + * QTM's expander management thread repeatedly writes (on every loop iteration) the current mask pattern + * plus polarity bit, whether it is normally set or overridden by `qtm:c`, then on the following set, + * negates both (it writes pattern ^ 0x1FFF). This is done at all times, even it 2D mode. + * + * The register being written to are regId 0x02 and 0x03 (output ports). + * TI TCA6416A I2C->Parallel expander is located on bus I2C1 (PA 0x10161000) device ID 0x40. + * + * This function has no effect on N2DSXL. + * + * @param pattern Barrier mask pattern (bit12: polarity, bit11-0: 12-bit mask pattern) + * @return `0xD82183F8` if API not started, otherwise 0 (success). + * @see Patent US20030234980A1 for a description of parallax barriers. + * @example The mask pattern used for super-stable 3D are as follows (position 0 to 11): + * + * 000011111100 + * 000001111110 + * 000000111111 + * 100000011111 + * 110000001111 + * 111000000111 + * 111100000011 + * 111110000001 + * 111111000000 + * 011111100000 + * 001111110000 + * 000111111000 + * + * When SS3D is disabled (ie. it tries to match O3DS behavior), then pattern becomes: + * 111100000111 + * Notice that the slit width is reduced from 6 to 5 units there. + * + * For 2D it is all-zero: + * 000000000000 + * + * 2D pattern is automatically set on QTM process init and exit. + */ +Result QTMC_SetBarrierPattern(u32 pattern); + +/** + * @brief Waits for the expander management thread to (re)initalize the TI TCA6416A I2C->Parallel expander, + * then checks if that expander is behaving as expected (responds with the port direction config + * it has been configured with): it checks whether all ports have been configured as outputs. + * + * On N2DSXL, this function waits forever and never returns. + * + * In detail, the hardware init procedure for the expander is as follows (as done by the expander mgmt. thread): + * - configure enable expander pin on SoC: set GPIO3.bit11 to OUTPUT, then set to 1 + * - on the expander (I2C1 deviceId 0x40), set all ports to OUTPUT (regId 0x06, 0x07) + * - on the expander, write 0 (all-transparent mask/2D) to the data registers (regId 0x02, 0x03) + * + * @param[out] outWorking Where to write the working status to. If true, expander is present working. + * If false, the expander is present but is misbehaving. If the function does not + * return, then expander is missing (e.g. on N2DSXL). + * @return `0xD82183F8` if API not started, otherwise 0 (success). + */ +Result QTMC_WaitAndCheckExpanderWorking(bool *outWorking); + +/** + * @brief Temporarily overrides IR LED state. Requires "manual control" from `qtm:u` to be disabled, and has + * lower priority than it. Same implementation as \ref QTMS_SetIrLedStatusOverride. + * + * @param on Whether to turn the IR LED on or off. + * @return `0xD82183F8` if API not started, `0xC8A18005` if manual control was enabled or if the operation failed, + * or `0xC8A18009` if QTM is unavailable (in particular, QTM is always unavailable on N2DSXL). Otherwise, 0 (success). + */ +Result QTMC_SetIrLedStatusOverride(bool on); diff --git a/libctru/source/services/qtm.c b/libctru/source/services/qtm.c index e8bc1b827..3c08539e5 100644 --- a/libctru/source/services/qtm.c +++ b/libctru/source/services/qtm.c @@ -7,76 +7,389 @@ #include <3ds/synchronization.h> #include <3ds/services/qtm.h> #include <3ds/ipc.h> +#include Handle qtmHandle; static int qtmRefCount; -Result qtmInit(void) +bool qtmCheckServicesRegistered(void) { - Result ret=0; + bool registered = false; + return R_SUCCEEDED(srvIsServiceRegistered(®istered, "qtm:u")) && registered; +} + +Result qtmInit(QtmServiceName serviceName) +{ + const char *name = NULL; + switch (serviceName) + { + default: + case QTM_SERVICE_USER: + name = "qtm:u"; + break; + case QTM_SERVICE_SYSTEM: + name = "qtm:s"; + break; + case QTM_SERVICE_SYSTEM_PROCESS: + name = "qtm:sp"; + break; + } if (AtomicPostIncrement(&qtmRefCount)) return 0; + Result res = srvGetServiceHandle(&qtmHandle, name); - ret = srvGetServiceHandle(&qtmHandle, "qtm:u"); - if (R_FAILED(ret)) ret = srvGetServiceHandle(&qtmHandle, "qtm:s"); - if (R_FAILED(ret)) ret = srvGetServiceHandle(&qtmHandle, "qtm:sp"); - if (R_FAILED(ret)) AtomicDecrement(&qtmRefCount); - return ret; + if (R_FAILED(res)) + { + qtmHandle = 0; + AtomicDecrement(&qtmRefCount); + } + + return res; } void qtmExit(void) { if (AtomicDecrement(&qtmRefCount)) return; svcCloseHandle(qtmHandle); + qtmHandle = 0; +} + +bool qtmIsInitialized(void) +{ + // Use qtmHandle instead of qtmRefCount in the event user + // wants to bypass qtmInit and use *qtmGetSessionHandle() instead + // (e.g. stolen session). + return qtmHandle != 0; +} + +Handle *qtmGetSessionHandle(void) +{ + return &qtmHandle; +} + +Result QTMU_GetRawTrackingDataEx(QtmRawTrackingData *outData, s64 predictionTimePointOrZero) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x1, 2, 0); // 0x10080 + memcpy(&cmdbuf[1], &predictionTimePointOrZero, 8); + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + memcpy(outData, &cmdbuf[2], sizeof(QtmRawTrackingData)); + + return cmdbuf[1]; +} + +Result QTMU_GetTrackingDataEx(QtmTrackingData *outData, s64 predictionTimePointOrZero) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x2, 2, 0); // 0x20080 + memcpy(&cmdbuf[1], &predictionTimePointOrZero, 8); + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + memcpy(outData, &cmdbuf[2], sizeof(QtmTrackingData)); + + return cmdbuf[1]; +} + +float qtmComputeFovX(const QtmTrackingData *data) +{ + return 2.0f * atanf(data->eyeWorldCoordinates[QTM_EYE_LEFT][0] / data->eyeCameraCoordinates[QTM_EYE_LEFT][0]); +} + +float qtmComputeFovY(const QtmTrackingData *data) +{ + return 2.0f * atanf(data->eyeWorldCoordinates[QTM_EYE_LEFT][1] / data->eyeCameraCoordinates[QTM_EYE_LEFT][1]); +} + +float qtmComputeInverseAspectRatio(const QtmTrackingData *data) +{ + return + (data->eyeWorldCoordinates[QTM_EYE_LEFT][1] * data->eyeCameraCoordinates[QTM_EYE_LEFT][0]) / + (data->eyeCameraCoordinates[QTM_EYE_LEFT][1] * data->eyeWorldCoordinates[QTM_EYE_LEFT][0]); +} + +float qtmComputeHeadTiltAngle(const QtmTrackingData *data) +{ + return atan2f( + data->eyeCameraCoordinates[QTM_EYE_RIGHT][1] - data->eyeCameraCoordinates[QTM_EYE_LEFT][1], + data->eyeCameraCoordinates[QTM_EYE_RIGHT][0] - data->eyeCameraCoordinates[QTM_EYE_LEFT][0] + ); +} + +float qtmEstimateEyeToCameraDistance(const QtmTrackingData *data) +{ + float fovX = qtmComputeFovX(data); + float pxDistNorm = hypotf( + data->eyeWorldCoordinates[QTM_EYE_RIGHT][0] - data->eyeWorldCoordinates[QTM_EYE_LEFT][0], + data->eyeWorldCoordinates[QTM_EYE_RIGHT][1] - data->eyeWorldCoordinates[QTM_EYE_LEFT][1] + ); + + return 31.0f / tanf(0.5f * (pxDistNorm * fovX)); +} + +Result QTMU_EnableManualIrLedControl(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x3, 0, 0); // 0x30000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMU_DisableManualIrLedControl(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x4, 0, 0); // 0x40000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMU_SetIrLedStatus(bool on) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x5, 1, 0); // 0x50040 + cmdbuf[1] = on; + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result qtmClearIrLedOverrides(void) +{ + QTMU_EnableManualIrLedControl(); + return QTMU_DisableManualIrLedControl(); +} + +Result QTMU_IsCurrentAppBlacklisted(bool *outBlacklisted) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x6, 0, 0); // 0x60000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + *outBlacklisted = (bool)(cmdbuf[2] & 1); + + return cmdbuf[1]; +} + +Result QTMS_SetCentralBarrierPosition(float position) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x401, 1, 0); // 0x4010040 + memcpy(&cmdbuf[1], &position, sizeof(float)); + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMS_GetCameraLuminance(float *outLuminanceLux) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x402, 0, 0); // 0x4020000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + memcpy(outLuminanceLux, &cmdbuf[2], sizeof(float)); + + return cmdbuf[1]; +} + +Result QTMS_EnableAutoBarrierControl(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x403, 0, 0); // 0x4030000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMS_DisableAutoBarrierControl(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x404, 0, 0); // 0x4040000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMS_SetBarrierPosition(u8 position) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x405, 1, 0); // 0x4050040 + cmdbuf[1] = position; + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMS_GetCurrentBarrierPosition(u8 *outPosition) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x406, 0, 0); // 0x4060000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + *outPosition = (u8)cmdbuf[2]; + + return cmdbuf[1]; } -bool qtmCheckInitialized(void) +Result QTMS_SetIrLedStatusOverride(bool on) { - return qtmRefCount>0; + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x407, 1, 0); // 0x4070040 + cmdbuf[1] = on; + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; } -bool qtmCheckHeadFullyDetected(QTM_HeadTrackingInfo *info) +Result QTMS_SetCalibrationData(const QtmCalibrationData *cal, bool saveCalToCfg) { - if(info==NULL)return false; + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x408, 7, 0); // 0x40801C0 + + memcpy(&cmdbuf[1], cal, sizeof(QtmCalibrationData)); + cmdbuf[7] = saveCalToCfg; + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMS_GetQtmStatus(QtmStatus *outQtmStatus) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x409, 0, 0); // 0x4090000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + *outQtmStatus = (QtmStatus)cmdbuf[2]; - if(info->flags[0] && info->flags[1] && info->flags[2])return true; - return false; + return cmdbuf[1]; } -Result qtmConvertCoordToScreen(QTM_HeadTrackingInfoCoord *coord, float *screen_width, float *screen_height, u32 *x, u32 *y) +Result QTMS_SetQtmStatus(QtmStatus qtmStatus) { - float width = 200.0f; - float height = 160.0f; + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x40A, 1, 0); // 0x40A0040 + cmdbuf[1] = qtmStatus; + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; - if(coord==NULL || x==NULL || y==NULL)return -1; + return cmdbuf[1]; +} - if(screen_width)width = (*screen_width) / 2; - if(screen_height)height = (*screen_height) / 2; +Result QTMSP_NotifyTopLcdModeChange(u8 newMode) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); - if(coord->x > 1.0f || coord->x < -1.0f)return -2; - if(coord->y > 1.0f || coord->y < -1.0f)return -2; + cmdbuf[0] = IPC_MakeHeader(0x801, 1, 0); // 0x8010040 + cmdbuf[1] = newMode; - *x = (u32)((coord->x * width) + width); - *y = (u32)((coord->y * height) + height); + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; - return 0; + return cmdbuf[1]; } -Result QTM_GetHeadTrackingInfo(u64 val, QTM_HeadTrackingInfo* out) +Result QTMSP_NotifyTopLcdPowerOn(void) { - if(!qtmCheckInitialized())return -1; + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); - Result ret = 0; - u32* cmdbuf = getThreadCommandBuffer(); + cmdbuf[0] = IPC_MakeHeader(0x802, 0, 0); // 0x8020000 - cmdbuf[0] = IPC_MakeHeader(0x2,2,0); // 0x20080 - cmdbuf[1] = val&0xFFFFFFFF; - cmdbuf[2] = val>>32; + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; - if(R_FAILED(ret=svcSendSyncRequest(qtmHandle)))return ret; + return cmdbuf[1]; +} - if(out) memcpy(out, &cmdbuf[2], sizeof(QTM_HeadTrackingInfo)); +Result QTMSP_IsExpanderInUse(bool *outInUse) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x803, 0, 0); // 0x8040000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + *outInUse = (bool)(cmdbuf[2] & 1); return cmdbuf[1]; } +Result QTMSP_NotifyTopLcdPowerOff(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x804, 0, 0); // 0x8040000 + + res = svcSendSyncRequest(qtmHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} diff --git a/libctru/source/services/qtmc.c b/libctru/source/services/qtmc.c new file mode 100644 index 000000000..13babc995 --- /dev/null +++ b/libctru/source/services/qtmc.c @@ -0,0 +1,101 @@ +#include +#include +#include <3ds/types.h> +#include <3ds/result.h> +#include <3ds/svc.h> +#include <3ds/srv.h> +#include <3ds/synchronization.h> +#include <3ds/services/qtmc.h> +#include <3ds/ipc.h> + +static Handle qtmcHandle; +static int qtmcRefCount; + +Result qtmcInit(void) +{ + if (AtomicPostIncrement(&qtmcRefCount)) return 0; + Result res = srvGetServiceHandle(&qtmcHandle, "qtm:c"); + + if (R_FAILED(res)) AtomicDecrement(&qtmcRefCount); + return res; +} + +void qtmcExit(void) +{ + if (AtomicDecrement(&qtmcRefCount)) return; + svcCloseHandle(qtmcHandle); +} + +Handle *qtmcGetSessionHandle(void) +{ + return &qtmcHandle; +} + +Result QTMC_StartHardwareCheck(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x1, 0, 0); // 0x10000 + + res = svcSendSyncRequest(qtmcHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMC_StopHardwareCheck(void) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x2, 0, 0); // 0x20000 + + res = svcSendSyncRequest(qtmcHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMC_SetBarrierPattern(u32 pattern) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x3, 1, 0); // 0x30040 + cmdbuf[1] = pattern; + + res = svcSendSyncRequest(qtmcHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +} + +Result QTMC_WaitAndCheckExpanderWorking(bool *outWorking) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x4, 0, 0); // 0x40000 + + res = svcSendSyncRequest(qtmcHandle); + if (R_FAILED(res)) return res; + + *outWorking = (bool)(cmdbuf[2] & 1); + + return cmdbuf[1]; +} + +Result QTMC_SetIrLedStatusOverride(bool on) +{ + Result res = 0; + u32 *cmdbuf = getThreadCommandBuffer(); + + cmdbuf[0] = IPC_MakeHeader(0x5, 1, 0); // 0x50040 + cmdbuf[1] = (u32)on; + + res = svcSendSyncRequest(qtmcHandle); + if (R_FAILED(res)) return res; + + return cmdbuf[1]; +}