diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e9879c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.o
+libs/
+obj/
+.vscode/
diff --git a/libretro-common/include/libretro.h b/libretro-common/include/libretro.h
index 3def427..f791f19 100644
--- a/libretro-common/include/libretro.h
+++ b/libretro-common/include/libretro.h
@@ -2871,6 +2871,336 @@ enum retro_mod
*/
#define RETRO_ENVIRONMENT_GET_HDR_MAX_NITS (92 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+/**
+ * Negotiates multi-channel audio output.
+ *
+ * The classic batch callbacks carry interleaved stereo. A core whose
+ * source has more channels - a console with discrete surround, a
+ * media player, an arcade board with a distinct rear pair - has had
+ * to fold them to two at the libretro boundary. This call hands the
+ * core a pair of batch entry points that take a frame of any of the
+ * layouts below, so the channels reach the frontend as they are;
+ * what happens to them then is the frontend's: sent discretely to a
+ * device that has those speakers, folded to stereo for one that does
+ * not, folded and re-expanded as the user's settings say.
+ *
+ * On success the frontend fills the supplied
+ * \c retro_audio_sample_multi_callback: \c batch_int16 always, and
+ * \c batch_float when it also answers \c true to
+ * \c RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT (a core wanting
+ * float should query that first; a NULL \c batch_float means int16
+ * only). Either function takes interleaved frames of \c channels
+ * samples, in the ascending-bit order of \c layout - front left,
+ * front right, front centre, LFE, back left, back right, ...
+ * - which is the order the WAVEFORMATEXTENSIBLE channel mask, ALSA,
+ * SDL and the WAV format use. \c channels must equal the number of
+ * bits set in \c layout. The return value has the meaning of
+ * \c retro_audio_sample_batch_t.
+ *
+ * Contract:
+ * - Negotiate once, during \c retro_load_game(). The layout may
+ * change from call to call (a game switching from stereo to 5.1),
+ * but the core commits to one sample format for the loaded game,
+ * as with the float call, and does not mix these entry points
+ * with the classic ones.
+ * - A layout with a bit the frontend does not know, or more than
+ * eight channels, is refused: the call returns 0 frames. Cores
+ * should use the \c RETRO_AUDIO_LAYOUT_ constants.
+ * - The function pointers are owned by the frontend and remain
+ * valid until \c retro_unload_game().
+ * - Frontends that do not recognise this call return \c false; the
+ * core keeps folding to stereo and using the classic callbacks.
+ *
+ * @param[out] data struct retro_audio_sample_multi_callback *.
+ * @return \c true if multi-channel output is supported, \c false otherwise.
+ * @see retro_audio_sample_multi_callback
+ * @see RETRO_AUDIO_SPEAKER_FRONT_LEFT
+ */
+#define RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI (94 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+/**
+ * Notifies the frontend that this core can render stereoscopic VR content
+ * and describes what it needs from the frontend to do so.
+ *
+ * Should be called from retro_load_game(), after RETRO_ENVIRONMENT_SET_HW_RENDER
+ * if hardware rendering is used. VR content requires hardware rendering in
+ * practice, since the frontend provides a hardware framebuffer containing
+ * both eye images.
+ *
+ * A core that supports both a flat and a VR presentation of the same
+ * content should call this unconditionally and let the frontend's return
+ * value decide: returning false means no VR session is available (headset
+ * build not active, or the platform does not support VR), in which case
+ * the core continues exactly as if the call had never been made.
+ *
+ * When VR is requested successfully, the core must report its VR framebuffer
+ * geometry through retro_get_system_av_info(): the base and maximum geometry
+ * must both be 2W by H, where W and H are the dimensions of one eye.
+ *
+ * The frontend uses this geometry when allocating the hardware framebuffer;
+ * recommended_eye_width and recommended_eye_height do not determine the
+ * framebuffer allocation.
+ *
+ * If RETRO_VR_FRAME_TARGET_RESIZED is subsequently reported, the eye target
+ * dimensions have changed. The core must call this environment command again
+ * and update its system AV info so that the base and maximum geometry are
+ * again 2W by H. Otherwise the new stereo framebuffer may exceed the
+ * currently allocated maximum geometry.
+ *
+ * The frontend samples the eye poses and field of view once per frame and
+ * passes that same sample to the core through
+ * RETRO_ENVIRONMENT_GET_VR_FRAME_STATE. When submitting the compositor
+ * layer for that frame, the frontend must use the same eye poses and FOV
+ * that were supplied to the core. This keeps the compositor submission
+ * synchronized with the sample used to render the eye images and avoids
+ * reprojection errors caused by using a different pose sample.
+ *
+ * @param[in,out] data struct retro_vr_content_info *.
+ * If non-NULL, requests VR presentation using the supplied content
+ * information. If NULL, disables the current VR presentation.
+ *
+ * @returns true if the requested operation was accepted and successfully
+ * applied, false if VR is unavailable or the request cannot be satisfied.
+ *
+ * @see retro_vr_content_info
+ * @see RETRO_ENVIRONMENT_GET_VR_FRAME_STATE
+ */
+#define RETRO_ENVIRONMENT_SET_VR_CONTENT_INFO (95 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+enum retro_vr_layout {
+ /* Core renders both eyes into the frontend framebuffer (the one from
+ * retro_hw_render_callback::get_current_framebuffer), left eye in
+ * x=[0,W), right eye in x=[W,2W), and calls retro_video_refresh_t ONCE per
+ * retro_run() with RETRO_HW_FRAME_BUFFER_VALID, width=2W, height=H.
+ * Origin follows hw_render.bottom_left_origin. Alpha is ignored. */
+ RETRO_VR_LAYOUT_SIDE_BY_SIDE = 0,
+ RETRO_VR_LAYOUT_DUMMY = INT_MAX
+};
+
+enum retro_vr_reference_space {
+ RETRO_VR_REFERENCE_SPACE_LOCAL = 0, /* seated: origin = head at session start/recenter */
+ RETRO_VR_REFERENCE_SPACE_STAGE = 1, /* standing: origin = floor */
+ RETRO_VR_REFERENCE_SPACE_DUMMY = INT_MAX
+};
+
+/**
+ * Details a core provides when requesting a VR session via
+ * RETRO_ENVIRONMENT_SET_VR_CONTENT_INFO.
+ */
+struct retro_vr_content_info
+{
+ /**
+ * true if the core renders both eyes natively into the VR
+ * framebuffer using the layout specified by layout
+ *
+ * false if the core renders mono content and the frontend
+ * derives the stereo presentation.
+ */
+ bool stereo_native;
+
+ /**
+ * A core has requested a flat presentation of its content, even if it
+ * is capable of producing VR output.
+ */
+ bool request_flat;
+
+ /**
+ * Requested interpupillary distance in meters, used only if the
+ * frontend cannot obtain one from the runtime/HMD itself.
+ *
+ * 0.0f means "use whatever the frontend/runtime already knows".
+ */
+ float ipd_hint_m;
+
+ enum retro_vr_layout layout;
+ enum retro_vr_reference_space reference_space;
+
+ /**
+ * Output: recommended width of one eye's render target.
+ *
+ * The core must not use this value to determine its system AV geometry.
+ * The VR framebuffer geometry is reported separately through
+ * retro_get_system_av_info() as 2W x H.
+ */
+ unsigned recommended_eye_width;
+
+ /**
+ * Output: recommended height of one eye's render target.
+ */
+ unsigned recommended_eye_height;
+};
+
+#define RETRO_VR_FRAME_RECENTERED (1u << 0) /* user recentered: re-capture any reference */
+#define RETRO_VR_FRAME_TARGET_RESIZED (1u << 1) /* eye size changed: call SET_VR_CONTENT_INFO again */
+
+/**
+ * Retrieves the per-eye state plus per-frame event flags
+ * (RETRO_VR_FRAME_RECENTERED / RETRO_VR_FRAME_TARGET_RESIZED) for the
+ * current retro_run() call.
+ *
+ * The frontend samples both eye states once per frame, immediately before
+ * retro_run(), and returns that same sample for the duration of that
+ * retro_run() call.
+ *
+ * The position and orientation in each eye state are the eye-to-tracking
+ * transforms used for that frame. The FOV values are the same values the
+ * frontend must use when submitting the compositor layer for the
+ * corresponding frame.
+ *
+ * @param[out] data struct retro_vr_frame_state *.
+ * @return false if no VR session exists or no valid sample is available.
+ */
+#define RETRO_ENVIRONMENT_GET_VR_FRAME_STATE (96 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+
+/**
+ * Identifies which eye a \ref retro_vr_eye_state describes, and the index
+ * of that eye within the two-element array passed to
+ * RETRO_ENVIRONMENT_GET_VR_FRAME_STATE.
+ */
+enum retro_vr_eye
+{
+ RETRO_VR_EYE_LEFT = 0,
+ RETRO_VR_EYE_RIGHT = 1,
+
+ /** @private Defined to ensure sizeof(retro_vr_eye) == sizeof(int). Do not use. */
+ RETRO_VR_EYE_DUMMY = INT_MAX
+};
+
+/**
+ * One eye's head-tracked pose and asymmetric field of view for the current
+ * frame, in the frontend's tracking space (meters, right-handed, Y up).
+ *
+ * position and orientation describe the eye-to-tracking-space transform,
+ * not its inverse.
+ *
+ * The quaternion is stored in (x, y, z, w) order. This is intentionally
+ * different from APIs such as GLM where glm::quat is constructed as
+ * (w, x, y, z).
+ *
+ * @see RETRO_ENVIRONMENT_GET_VR_FRAME_STATE
+ */
+struct retro_vr_eye_state
+{
+ /**
+ * Eye position in tracking space, in meters.
+ */
+ float position[3];
+
+ /**
+ * Eye orientation in tracking space, as a quaternion (x, y, z, w).
+ *
+ * This is the orientation of the eye relative to tracking space, not
+ * the inverse/view orientation.
+ */
+ float orientation[4];
+
+ /**
+ * Tangent of the positive half-angle from the eye's forward axis to
+ * each frustum edge, in the order:
+ *
+ * [0] left
+ * [1] right
+ * [2] up
+ * [3] down
+ *
+ * All four values are positive magnitudes. They describe the distance
+ * of each frustum edge from the forward axis in tangent space; they are
+ * not signed OpenXR angles.
+ *
+ * The four values are independent because HMD eye frustums are normally
+ * asymmetric.
+ *
+ * For example, if the left and right tangent values are l and r, and
+ * the up and down tangent values are u and d, the projection frustum
+ * spans:
+ *
+ * left = -l
+ * right = r
+ * bottom = -d
+ * top = u
+ *
+ * A frontend converting from APIs that represent the corresponding
+ * angles as signed values must use their magnitudes.
+ */
+ float fov_tan[4];
+};
+
+struct retro_vr_frame_state {
+ struct retro_vr_eye_state eyes[2];
+ uint32_t flags;
+};
+
+/**
+ * Retrieves the current head-tracked pose.
+ *
+ * The frontend samples the HMD once per frame, immediately before
+ * retro_run(), and returns the same sample for the duration of that
+ * retro_run() call.
+ */
+#define RETRO_ENVIRONMENT_GET_VR_HEAD_POSE (97 | RETRO_ENVIRONMENT_EXPERIMENTAL)
+
+#define RETRO_VR_HEAD_POSE_POSITION_VALID (1u << 0)
+#define RETRO_VR_HEAD_POSE_ORIENTATION_VALID (1u << 1)
+#define RETRO_VR_HEAD_POSE_VELOCITY_VALID (1u << 2)
+
+/**
+ * Head pose and motion in the frontend's tracking space (meters,
+ * seconds, right-handed, Y up).
+ *
+ * position and orientation describe the head-to-tracking-space transform,
+ * not its inverse. The orientation quaternion rotates vectors from head
+ * space into tracking space.
+ *
+ * linear_velocity is the velocity of the head-space origin, expressed in
+ * tracking-space coordinates. angular_velocity is the head's angular
+ * velocity, expressed in tracking-space coordinates. Velocity fields are
+ * valid only when RETRO_VR_HEAD_POSE_VELOCITY_VALID is set.
+ */
+struct retro_vr_head_pose
+{
+ /** Head position in tracking space, in meters. */
+ float position[3];
+
+ /** Head orientation in tracking space, quaternion (x, y, z, w). */
+ float orientation[4];
+
+ /** Linear velocity in meters/second. */
+ float linear_velocity[3];
+
+ /** Angular velocity in radians/second. */
+ float angular_velocity[3];
+
+ /* orientation/position are valid */
+ uint32_t flags;
+};
+
+/* Speaker positions, as bits of a layout mask; a frame's channels are
+ * interleaved in ascending bit order. The bits are those of the
+ * WAVEFORMATEXTENSIBLE channel mask. */
+#define RETRO_AUDIO_SPEAKER_FRONT_LEFT 0x001
+#define RETRO_AUDIO_SPEAKER_FRONT_RIGHT 0x002
+#define RETRO_AUDIO_SPEAKER_FRONT_CENTER 0x004
+#define RETRO_AUDIO_SPEAKER_LOW_FREQUENCY 0x008
+#define RETRO_AUDIO_SPEAKER_BACK_LEFT 0x010
+#define RETRO_AUDIO_SPEAKER_BACK_RIGHT 0x020
+#define RETRO_AUDIO_SPEAKER_FRONT_LEFT_OF_CENTER 0x040
+#define RETRO_AUDIO_SPEAKER_FRONT_RIGHT_OF_CENTER 0x080
+#define RETRO_AUDIO_SPEAKER_BACK_CENTER 0x100
+#define RETRO_AUDIO_SPEAKER_SIDE_LEFT 0x200
+#define RETRO_AUDIO_SPEAKER_SIDE_RIGHT 0x400
+
+/* The layouts a core is expected to use. Others are accepted where the
+ * frontend knows every bit. */
+#define RETRO_AUDIO_LAYOUT_MONO (RETRO_AUDIO_SPEAKER_FRONT_CENTER)
+#define RETRO_AUDIO_LAYOUT_STEREO (RETRO_AUDIO_SPEAKER_FRONT_LEFT | RETRO_AUDIO_SPEAKER_FRONT_RIGHT)
+#define RETRO_AUDIO_LAYOUT_2_1 (RETRO_AUDIO_LAYOUT_STEREO | RETRO_AUDIO_SPEAKER_LOW_FREQUENCY)
+#define RETRO_AUDIO_LAYOUT_QUAD (RETRO_AUDIO_LAYOUT_STEREO | RETRO_AUDIO_SPEAKER_BACK_LEFT | RETRO_AUDIO_SPEAKER_BACK_RIGHT)
+#define RETRO_AUDIO_LAYOUT_5_1 (RETRO_AUDIO_LAYOUT_QUAD | RETRO_AUDIO_SPEAKER_FRONT_CENTER | RETRO_AUDIO_SPEAKER_LOW_FREQUENCY)
+#define RETRO_AUDIO_LAYOUT_5_1_SIDE (RETRO_AUDIO_LAYOUT_STEREO | RETRO_AUDIO_SPEAKER_FRONT_CENTER | RETRO_AUDIO_SPEAKER_LOW_FREQUENCY \
+ | RETRO_AUDIO_SPEAKER_SIDE_LEFT | RETRO_AUDIO_SPEAKER_SIDE_RIGHT)
+#define RETRO_AUDIO_LAYOUT_7_1 (RETRO_AUDIO_LAYOUT_5_1 | RETRO_AUDIO_SPEAKER_SIDE_LEFT | RETRO_AUDIO_SPEAKER_SIDE_RIGHT)
+
/**
* Result of \c RETRO_ENVIRONMENT_GET_MEMORY_STATUS.
*
@@ -3052,8 +3382,49 @@ struct retro_vfs_dir_handle;
*/
#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2)
+/**
+ * Indicates that the current user cannot write to the given path.
+ * POSIX: the owner write bit is clear.
+ * Windows/UWP: \c FILE_ATTRIBUTE_READONLY is set.
+ * Frontends that cannot determine this never set the flag.
+ * @since VFS API v5
+ */
+#define RETRO_VFS_STAT_IS_READONLY (1 << 3)
+
/** @} */
+/**
+ * @defgroup RETRO_VFS_COPY Copy Flags
+ * @since VFS API v5
+ * @{
+ */
+
+/** Replace \c dst if it already exists. Without it an existing \c dst is an error. */
+#define RETRO_VFS_COPY_OVERWRITE (1 << 0)
+
+/** @} */
+
+/**
+ * @defgroup RETRO_VFS_COPY_STATUS Copy Status
+ * Values returned by \c retro_vfs_copy_step_t.
+ * @since VFS API v5
+ * @{
+ */
+/** The copy is still in progress. */
+#define RETRO_VFS_COPY_RUNNING (0)
+/** The copy completed; \c dst is complete and closed. */
+#define RETRO_VFS_COPY_DONE (1)
+/** The copy failed or was cancelled; no partial \c dst remains. */
+#define RETRO_VFS_COPY_FAILED (-1)
+/** @} */
+
+/**
+ * Opaque handle to an in-progress file copy.
+ * @see retro_vfs_copy_begin_t
+ * @since VFS API v5
+ */
+struct retro_vfs_copy_handle;
+
/**
* Returns the path that was used to open this file.
*
@@ -3245,6 +3616,124 @@ typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size);
*/
typedef int (RETRO_CALLCONV *retro_vfs_stat_64_t)(const char *path, int64_t *size);
+/**
+ * Sets or clears the read-only state of a file or directory.
+ *
+ * POSIX: sets or clears the write bits of the mode, leaving the rest intact.
+ * Windows/UWP: sets or clears \c FILE_ATTRIBUTE_READONLY.
+ *
+ * @param path The path to the file or directory.
+ * @param readonly Non-zero to make the path read-only,
+ * zero to make it writable.
+ * @return 0 on success,
+ * or -1 if \c path does not exist or the platform or file system
+ * cannot store a read-only state.
+ * @see path_set_readonly
+ * @see RETRO_VFS_STAT_IS_READONLY
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_set_readonly_t)(const char *path, int readonly);
+
+/**
+ * Gets the last modification time of a file or directory.
+ *
+ * @param path The path to the file or directory.
+ * @param[out] mtime Set to the modification time
+ * in seconds since 1970-01-01T00:00:00Z. May be negative.
+ * @return 0 on success,
+ * or -1 if \c path does not exist or the platform
+ * cannot report a modification time.
+ * @see path_get_mtime
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_get_mtime_t)(const char *path, int64_t *mtime);
+
+/**
+ * Sets the last modification time of a file or directory.
+ *
+ * The frontend rounds to the file system's resolution,
+ * so a following \c retro_vfs_get_mtime_t may report a different value.
+ *
+ * @param path The path to the file or directory.
+ * @param mtime The modification time in seconds since 1970-01-01T00:00:00Z.
+ * @return 0 on success,
+ * or -1 if \c path does not exist or the platform or file system
+ * does not allow the modification time to be set.
+ * @see path_set_mtime
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_set_mtime_t)(const char *path, int64_t mtime);
+
+/**
+ * Starts copying a single regular file and returns without moving any of it.
+ *
+ * A copy is a resumable operation that the caller advances with
+ * \c retro_vfs_copy_step_t, each step bounded by a byte budget the caller
+ * chooses. The frontend keeps no thread and holds no lock for it; a caller
+ * that wants the transfer off its own thread drives the steps from wherever
+ * it likes. No call in this group ever waits for more than the requested
+ * step.
+ *
+ * \c dst is the full path of the new file, not a directory; missing parent
+ * directories are created. Metadata (modification time, read-only state)
+ * of \c dst after the copy is platform-defined. Either path may belong to
+ * any file system the frontend supports.
+ *
+ * Checks that can be made up front (missing or non-regular \c src,
+ * \c dst is a directory, \c dst exists without \c RETRO_VFS_COPY_OVERWRITE,
+ * \c src equals \c dst) fail here by returning \c NULL.
+ *
+ * @param src The path to the file to copy. Must be a regular file.
+ * @param dst The full path of the destination file. Must differ from \c src.
+ * @param flags Bitwise combination of \c RETRO_VFS_COPY flags, or 0.
+ * @return A handle to poll and close, or \c NULL if the copy could not start.
+ * @see retro_vfs_copy_step_t
+ * @see retro_vfs_copy_close_t
+ * @see filestream_copy_begin
+ * @see RETRO_VFS_COPY
+ * @since VFS API v5
+ */
+typedef struct retro_vfs_copy_handle *(RETRO_CALLCONV *retro_vfs_copy_begin_t)(const char *src, const char *dst, unsigned flags);
+
+/**
+ * Advances a copy started with \c retro_vfs_copy_begin_t by at most
+ * \c max_bytes and reports its state.
+ *
+ * The budget is the caller's latency/throughput dial: a few MiB from a
+ * frame loop keeps each call short; a very large budget (or repeated calls
+ * until the status leaves \c RETRO_VFS_COPY_RUNNING) runs the transfer at
+ * the full speed of the platform's copy primitive with no user-space
+ * buffer where the kernel can move the bytes itself.
+ *
+ * A step never moves more than \c max_bytes, but it may move less, and it
+ * may report \c RETRO_VFS_COPY_DONE early if the platform completed the
+ * copy without moving bytes (e.g. a file-system clone).
+ *
+ * @param handle The copy.
+ * @param max_bytes Upper bound on bytes moved by this call; 0 selects a
+ * frontend default sized for a frame loop (a few MiB).
+ * @param[out] bytes_done Bytes written to \c dst so far. May be \c NULL.
+ * @param[out] bytes_total Size of \c src in bytes. May be \c NULL.
+ * @return One of the \c RETRO_VFS_COPY_STATUS values.
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_copy_step_t)(struct retro_vfs_copy_handle *handle, int64_t max_bytes, int64_t *bytes_done, int64_t *bytes_total);
+
+/**
+ * Releases a copy handle.
+ *
+ * If the copy is still running it is cancelled and the partial \c dst
+ * removed; nothing is waited for. Must be called exactly once for every
+ * non-NULL handle from \c retro_vfs_copy_begin_t, whatever
+ * \c retro_vfs_copy_step_t reported.
+ *
+ * @param handle The copy.
+ * @return 0 if the copy had completed successfully, or -1 if it failed,
+ * was cancelled, or was still running when closed.
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_copy_close_t)(struct retro_vfs_copy_handle *handle);
+
/**
* Creates a directory at the given path.
*
@@ -3317,6 +3806,29 @@ typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_v
*/
typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream);
+/**
+ * Gets information about the directory entry most recently returned by
+ * \c retro_vfs_readdir_t, without opening it or building its path.
+ *
+ * Only valid after a \c retro_vfs_readdir_t call that returned \c true,
+ * and before the next \c retro_vfs_readdir_t or \c retro_vfs_closedir_t
+ * call on the same handle.
+ *
+ * @param dirstream The directory being enumerated.
+ * @param[out] size The entry's size in bytes (0 for directories).
+ * May be \c NULL, in which case this value is ignored.
+ * @param[out] mtime The entry's modification time
+ * in seconds since 1970-01-01T00:00:00Z.
+ * May be \c NULL, in which case this value is ignored.
+ * @return A bitmask of \c RETRO_VFS_STAT flags for the entry
+ * (\c RETRO_VFS_STAT_IS_VALID is always set on success),
+ * or 0 if the frontend cannot provide entry information.
+ * @see retro_dirent_stat
+ * @see RETRO_VFS_STAT
+ * @since VFS API v5
+ */
+typedef int (RETRO_CALLCONV *retro_vfs_dirent_stat_t)(struct retro_vfs_dir_handle *dirstream, int64_t *size, int64_t *mtime);
+
/**
* Closes the given directory and release its resources.
*
@@ -3404,6 +3916,28 @@ struct retro_vfs_interface
/* VFS API v4 */
/** @copydoc retro_vfs_stat_64_t */
retro_vfs_stat_64_t stat_64;
+
+ /* VFS API v5 */
+ /** @copydoc retro_vfs_set_readonly_t */
+ retro_vfs_set_readonly_t set_readonly;
+
+ /** @copydoc retro_vfs_get_mtime_t */
+ retro_vfs_get_mtime_t get_mtime;
+
+ /** @copydoc retro_vfs_set_mtime_t */
+ retro_vfs_set_mtime_t set_mtime;
+
+ /** @copydoc retro_vfs_copy_begin_t */
+ retro_vfs_copy_begin_t copy_begin;
+
+ /** @copydoc retro_vfs_copy_step_t */
+ retro_vfs_copy_step_t copy_step;
+
+ /** @copydoc retro_vfs_copy_close_t */
+ retro_vfs_copy_close_t copy_close;
+
+ /** @copydoc retro_vfs_dirent_stat_t */
+ retro_vfs_dirent_stat_t dirent_stat;
};
/**
@@ -3755,6 +4289,20 @@ enum retro_hw_render_context_negotiation_interface_type
*/
RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0,
+ /**
+ * Denotes a context negotiation interface for Direct3D 12.
+ * Carries the highest hardware render interface version the core can use.
+ * @see retro_hw_render_context_negotiation_interface_d3d12
+ */
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_D3D12 = 1,
+
+ /**
+ * Denotes a context negotiation interface for Direct3D 11.
+ * Carries the highest hardware render interface version the core can use.
+ * @see retro_hw_render_context_negotiation_interface_d3d11
+ */
+ RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_D3D11 = 2,
+
/**
* @private Defined to ensure sizeof(retro_hw_render_context_negotiation_interface_type) == sizeof(int).
* Do not use.
@@ -8021,6 +8569,40 @@ struct retro_audio_sample_float_callback
retro_audio_sample_batch_float_t batch;
};
+/**
+ * Renders multiple audio frames of a multi-channel layout.
+ *
+ * Valid only after the frontend has answered \c true to
+ * \c RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI.
+ *
+ * @param data Interleaved frames of \c channels samples, one sample
+ * a speaker in the ascending-bit order of \c layout; int16 or
+ * float in [-1.0, 1.0] by the entry point.
+ * @param frames The number of frames in \c data.
+ * @param channels Samples per frame: the bits set in \c layout.
+ * @param layout The speaker mask, from the \c RETRO_AUDIO_SPEAKER_ bits.
+ * @return The number of frames processed; 0 for a layout the frontend
+ * does not take.
+ * @see RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI
+ */
+typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_multi_int16_t)(
+ const int16_t *data, size_t frames, unsigned channels, unsigned layout);
+typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_multi_float_t)(
+ const float *data, size_t frames, unsigned channels, unsigned layout);
+
+/**
+ * Multi-channel batch callbacks handed to the core in response to
+ * \c RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_MULTI.
+ */
+struct retro_audio_sample_multi_callback
+{
+ /* Set by the frontend. */
+ retro_audio_sample_batch_multi_int16_t batch_int16;
+ /* Set by the frontend when float output is negotiated too, NULL
+ * otherwise. */
+ retro_audio_sample_batch_multi_float_t batch_float;
+};
+
/**
* Polls input.
*
diff --git a/libretro.cpp b/libretro.cpp
index 10b5080..fde8e6f 100644
--- a/libretro.cpp
+++ b/libretro.cpp
@@ -71,6 +71,11 @@ unsigned engine_height = BASE_HEIGHT;
GLuint tex;
GLuint g_texture_target = GL_TEXTURE_2D;
+bool vr_active = false;
+unsigned vr_eye_width = 0;
+unsigned vr_eye_height = 0;
+bool vr_user_enable = true;
+
void retro_init(void)
{
struct retro_log_callback log;
@@ -119,6 +124,13 @@ void retro_get_system_av_info(struct retro_system_av_info *info)
info->geometry.base_height = BASE_HEIGHT;
info->geometry.max_width = MAX_WIDTH;
info->geometry.max_height = MAX_HEIGHT;
+
+ if (vr_active)
+ {
+ info->geometry.base_width = info->geometry.max_width = vr_eye_width * 2;
+ info->geometry.base_height = info->geometry.max_height = vr_eye_height;
+ info->geometry.aspect_ratio = (float)(vr_eye_width * 2) / vr_eye_height;
+ }
}
void retro_set_environment(retro_environment_t cb)
@@ -158,6 +170,7 @@ void retro_set_environment(retro_environment_t cb)
},
{ "3dengine-modelviewer-discard-hack", "Discard hack enable; disabled|enabled" },
{ "3dengine-location-display-position", "Location position OSD; disabled|enabled" },
+ { "3dengine-modelviewer-vr-enable", "VR enable in modelviewer; enabled|disabled" },
{ NULL, NULL },
};
@@ -404,6 +417,53 @@ static void location_deinitialized(void)
location_cb.stop();
}
+// VAR
+static bool vr_request_session(void)
+{
+ struct retro_vr_content_info vr;
+ memset(&vr, 0, sizeof(vr));
+ vr.stereo_native = true;
+ vr.ipd_hint_m = 0.0f;
+ vr.layout = RETRO_VR_LAYOUT_SIDE_BY_SIDE;
+ vr.request_flat = !vr_user_enable;
+ vr.reference_space = RETRO_VR_REFERENCE_SPACE_LOCAL; /* eye height == player_pos.y */
+
+ if (!environ_cb(RETRO_ENVIRONMENT_SET_VR_CONTENT_INFO, &vr))
+ {
+ log_cb(RETRO_LOG_INFO, "VR session request failed.\n");
+ return false; /* old frontend, no headset, or VR disabled -> flat */
+ }
+
+ vr_eye_width = vr.recommended_eye_width ? vr.recommended_eye_width : 1024;
+ vr_eye_height = vr.recommended_eye_height ? vr.recommended_eye_height : 1024;
+
+ log_cb(RETRO_LOG_INFO, "VR session request success. vr_eye_width: %d vr_eye_height: %d.\n", vr_eye_width, vr_eye_height);
+
+ return true;
+}
+
+bool vr_poll_frame(struct retro_vr_frame_state *fs)
+{
+ struct retro_vr_frame_state tmp = *fs;
+ if (!environ_cb(RETRO_ENVIRONMENT_GET_VR_FRAME_STATE, &tmp))
+ return false; /* keep last pose */
+ *fs = tmp;
+
+ if (tmp.flags & RETRO_VR_FRAME_TARGET_RESIZED)
+ {
+ if (vr_request_session())
+ {
+ struct retro_system_av_info av;
+ retro_get_system_av_info(&av);
+ environ_cb(RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO, &av);
+ }
+ else
+ vr_active = false;
+ }
+ return true;
+}
+// END VR
+
bool retro_load_game(const struct retro_game_info *info)
{
retro_variable var;
@@ -544,6 +604,11 @@ bool retro_load_game(const struct retro_game_info *info)
if (engine_program_cb && engine_program_cb->load_game)
engine_program_cb->load_game(info);
+ vr_active = false;
+ if (vr_user_enable && engine_program_cb == &engine_program_modelviewer &&
+ modelviewer_vr_capable())
+ vr_active = vr_request_session();
+
return true;
}
@@ -554,6 +619,7 @@ void retro_unload_game(void)
if (convert_buffer)
delete[] convert_buffer;
convert_buffer = NULL;
+ vr_active = false;
}
unsigned retro_get_region(void)
diff --git a/program.h b/program.h
index 2dfd425..702a17b 100644
--- a/program.h
+++ b/program.h
@@ -22,6 +22,8 @@
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
+#include "vr_support.hpp"
+
extern retro_input_poll_t input_poll_cb;
extern retro_input_state_t input_state_cb;
extern struct retro_hw_render_callback hw_render;
@@ -59,4 +61,11 @@ typedef struct engine_program
extern const engine_program_t engine_program_instancingviewer;
extern const engine_program_t engine_program_modelviewer;
+// VR
+extern bool vr_active;
+extern unsigned vr_eye_width;
+extern unsigned vr_eye_height;
+extern bool modelviewer_vr_capable(void);
+extern bool vr_poll_frame(struct retro_vr_frame_state *fs);
+
#endif
diff --git a/program/modelviewer.cpp b/program/modelviewer.cpp
index 0906e3d..bb9d5fc 100644
--- a/program/modelviewer.cpp
+++ b/program/modelviewer.cpp
@@ -59,12 +59,19 @@ enum
unsigned mode_engine = MODE_MODELVIEWER;
+static bool scene_flip_y(void) { return !hw_render.bottom_left_origin; }
+
+static struct retro_vr_frame_state vr_frame;
+bool modelviewer_vr_capable(void) { return mode_engine == MODE_SCENEWALKER; }
+
+static float player_view_deg_x;
+static float player_view_deg_y;
+static vec3 player_pos(0, 2, 0);
+
+extern bool vr_user_enable;
+
static vec3 scenewalker_check_input(void)
{
- static float player_view_deg_x;
- static float player_view_deg_y;
- static vec3 player_pos(0, 2, 0);
-
input_poll_cb();
int analog_x = input_state_cb(0, RETRO_DEVICE_ANALOG,
@@ -152,6 +159,9 @@ static vec3 scenewalker_check_input(void)
}
#endif
+ if (vr_active)
+ analog_ry = 0; /* head controls pitch */
+
player_view_deg_y += analog_rx * -0.00008f;
player_view_deg_x += analog_ry * -0.00005f;
@@ -166,6 +176,13 @@ static vec3 scenewalker_check_input(void)
vec3 right_walk_dir = vec3(rotate_y_right * vec4(0, 0, -1, 1));
vec3 front_walk_dir = vec3(rotate_y * vec4(0, 0, -1, 1));
+ if (vr_active)
+ {
+ /* walk where the head points, on the ground plane */
+ front_walk_dir = vec3(rotate_y * vec4(vr_head_forward_xz(vr_frame), 0.0f));
+ right_walk_dir = cross(front_walk_dir, vec3(0, 1, 0));
+ }
+
vec3 velocity = front_walk_dir * vec3(analog_y * -0.000002f) +
right_walk_dir * vec3(analog_x * 0.000002f);
@@ -539,9 +556,15 @@ static void init_mesh(const std::string& path)
mat4 projection;
if (mode_engine == MODE_SCENEWALKER)
- projection = scale(mat4(1.0), vec3(1, -1, 1)) * perspective(45.0f, 640.0f / 480.0f, 1.0f, 100.0f);
+ {
+ projection = perspective(45.0f, 640.0f / 480.0f, 1.0f, 100.0f);
+ if (scene_flip_y())
+ projection = scale(mat4(1.0), vec3(1, -1, 1)) * projection;
+ }
else
+ {
projection = scale(mat4(1.0), vec3(1, -1, 1)) * perspective(45.0f, 4.0f / 3.0f, 0.2f, 100.0f);
+ }
for (unsigned i = 0; i < meshes.size(); i++)
{
@@ -617,34 +640,92 @@ static void modelviewer_update_variables(retro_environment_t environ_cb)
if (!first_init)
modelviewer_context_reset();
}
+
+ var.key = "3dengine-modelviewer-vr-enable";
+ var.value = NULL;
+
+ if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
+ {
+ if (!strcmp(var.value, "disabled"))
+ vr_user_enable = false;
+ else if (!strcmp(var.value, "enabled"))
+ vr_user_enable = true;
+
+ if (!first_init)
+ modelviewer_context_reset();
+ }
}
static void modelviewer_run(void)
{
- unsigned i;
+ unsigned i, width, height;
+ const bool scene = (mode_engine == MODE_SCENEWALKER);
+ bool vr = false;
+
+ if (scene && vr_active)
+ {
+ vr_poll_frame(&vr_frame); /* once, before input, per the API doc */
+ vr = vr_active; /* may drop if the resize renegotiation failed */
+ }
+
vec3 look_dir = modelviewer_check_input();
(void)look_dir;
+ width = vr ? vr_eye_width * 2 : engine_width;
+ height = vr ? vr_eye_height : engine_height;
+
glBindFramebuffer(GL_FRAMEBUFFER, hw_render.get_current_framebuffer());
- glViewport(0, 0, engine_width, engine_height);
+ glViewport(0, 0, width, height);
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
- glFrontFace(GL_CW); // When we flip vertically, orientation changes.
+ /* Only the flipped projection needs CW. Unflipped is normal CCW. */
+ glFrontFace((scene && !scene_flip_y()) ? GL_CCW : GL_CW);
glEnable(GL_CULL_FACE);
glEnable(GL_BLEND);
- for (i = 0; i < meshes.size(); i++)
- meshes[i]->render();
+ if (vr)
+ {
+ unsigned eye;
+ mat4 rig = translate(mat4(1.0), player_pos) *
+ rotate(mat4(1.0), player_view_deg_y, vec3(0, 1, 0));
+
+ glEnable(GL_SCISSOR_TEST);
+ for (eye = 0; eye < 2; eye++)
+ {
+ const struct retro_vr_eye_state &es = vr_frame.eyes[eye];
+ mat4 eye_world = rig * vr_eye_pose_matrix(es);
+ mat4 view = inverse(eye_world);
+ mat4 proj = vr_projection(es.fov_tan, 0.1f, 100.0f);
+ vec3 eye_pos = vec3(eye_world[3]);
+
+ if (scene_flip_y())
+ proj = scale(mat4(1.0), vec3(1, -1, 1)) * proj;
+
+ glViewport(eye * vr_eye_width, 0, vr_eye_width, vr_eye_height);
+ glScissor (eye * vr_eye_width, 0, vr_eye_width, vr_eye_height);
+
+ for (i = 0; i < meshes.size(); i++)
+ {
+ meshes[i]->set_projection(proj);
+ meshes[i]->set_view(view);
+ meshes[i]->set_eye(eye_pos);
+ meshes[i]->render();
+ }
+ }
+ glDisable(GL_SCISSOR_TEST);
+ }
+ else
+ for (i = 0; i < meshes.size(); i++)
+ meshes[i]->render();
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
- video_cb(RETRO_HW_FRAME_BUFFER_VALID, engine_width, engine_height, 0);
+ video_cb(RETRO_HW_FRAME_BUFFER_VALID, width, height, 0);
}
-
static void scenewalker_reset_mesh_path(void)
{
//always assume matching .obj file is there
@@ -677,6 +758,8 @@ static void modelviewer_load_game(const struct retro_game_info *info)
player_size = vec3(0, 0, 0);
first_init = false;
+
+ vr_frame_defaults(&vr_frame);
}
const engine_program_t engine_program_modelviewer = {
diff --git a/vr_support.hpp b/vr_support.hpp
new file mode 100644
index 0000000..7fea2b1
--- /dev/null
+++ b/vr_support.hpp
@@ -0,0 +1,73 @@
+/*
+ * Libretro 3DEngine - VR helpers
+ *
+ */
+
+#ifndef VR_SUPPORT_HPP__
+#define VR_SUPPORT_HPP__
+
+#include
+#include
+#include "libretro.h"
+#include "glm/glm.hpp"
+#include "glm/gtc/matrix_transform.hpp"
+#include "glm/gtc/quaternion.hpp"
+
+/* Identity pose + 90 degree symmetric FOV. Used until the frontend gives us
+ * a valid sample (first frame), so we never normalize a zero quaternion. */
+static inline void vr_frame_defaults(struct retro_vr_frame_state *fs)
+{
+ unsigned i, j;
+ memset(fs, 0, sizeof(*fs));
+ for (i = 0; i < 2; i++)
+ {
+ fs->eyes[i].orientation[3] = 1.0f;
+ for (j = 0; j < 4; j++)
+ fs->eyes[i].fov_tan[j] = 1.0f;
+ }
+}
+
+static inline glm::quat vr_quat(const float q[4])
+{
+ /* libretro: (x, y, z, w). glm::quat constructor: (w, x, y, z). */
+ return glm::normalize(glm::quat(q[3], q[0], q[1], q[2]));
+}
+
+/* Eye -> tracking space transform. View matrix is the inverse of this
+ * (after applying the rig transform). */
+static inline glm::mat4 vr_eye_pose_matrix(const struct retro_vr_eye_state &e)
+{
+ glm::mat4 m = glm::mat4_cast(vr_quat(e.orientation));
+ m[3] = glm::vec4(e.position[0], e.position[1], e.position[2], 1.0f);
+ return m;
+}
+
+/* Asymmetric frustum from the four tangents. fabsf() so it works whether the
+ * frontend reports magnitudes or OpenXR-style signed angles
+ * (left/down negative) - see notes on fov_tan in the reply. */
+static inline glm::mat4 vr_projection(const float t[4], float znear, float zfar)
+{
+ float l = fabsf(t[0]) * znear;
+ float r = fabsf(t[1]) * znear;
+ float u = fabsf(t[2]) * znear;
+ float d = fabsf(t[3]) * znear;
+ return glm::frustum(-l, r, -d, u, znear, zfar);
+}
+
+/* Unit vector, in tracking space, of where the head is facing, flattened onto
+ * the ground plane. Averages both eyes (some HMDs cant their displays). */
+static inline glm::vec3 vr_head_forward_xz(const struct retro_vr_frame_state &fs)
+{
+ unsigned i;
+ glm::vec3 f(0.0f);
+
+ for (i = 0; i < 2; i++)
+ f += vr_quat(fs.eyes[i].orientation) * glm::vec3(0.0f, 0.0f, -1.0f);
+
+ f.y = 0.0f;
+ if (glm::length(f) < 0.001f) /* looking straight up/down */
+ return glm::vec3(0.0f, 0.0f, -1.0f);
+ return glm::normalize(f);
+}
+
+#endif