diff --git a/source/LibRender2/LibRender2.csproj b/source/LibRender2/LibRender2.csproj index dcea6d70e..01af987e8 100644 --- a/source/LibRender2/LibRender2.csproj +++ b/source/LibRender2/LibRender2.csproj @@ -122,6 +122,7 @@ + diff --git a/source/LibRender2/Textures/TextureManager.cs b/source/LibRender2/Textures/TextureManager.cs index a7eb1518a..07a547b50 100644 --- a/source/LibRender2/Textures/TextureManager.cs +++ b/source/LibRender2/Textures/TextureManager.cs @@ -58,12 +58,64 @@ public bool RegisterTexture(string path, out Texture handle) /// Whether registering the texture was successful. public bool RegisterTexture(string path, TextureParameters parameters, out Texture handle) { - if (!File.Exists(path)) + if (string.IsNullOrEmpty(path)) + { + handle = null; + return false; + } + + string cleanPath = path; + string queryString = ""; + int qIndex = path.IndexOf('?'); + if (qIndex >= 0) + { + cleanPath = path.Substring(0, qIndex); + queryString = path.Substring(qIndex + 1); + } + + // Block internet URLs and network shares to prevent security flaws + if (cleanPath.Contains("://") || cleanPath.StartsWith("\\\\") || cleanPath.StartsWith("//")) + { + handle = null; + return false; + } + + if (!File.Exists(cleanPath)) { // shouldn't happen, but handle gracefully handle = null; return false; } + + string ext = System.IO.Path.GetExtension(cleanPath).ToLowerInvariant(); + if (ext == ".mp4" || ext == ".mkv" || ext == ".webm" || ext == ".avi") + { + for (int i = 0; i < RegisteredTexturesCount; i++) + { + if (RegisteredTextures[i] != null && RegisteredTextures[i].VideoContext != null) + { + VideoTexture videoTex = RegisteredTextures[i].VideoContext as VideoTexture; + if (videoTex != null && videoTex.VideoPath.Equals(cleanPath, StringComparison.InvariantCultureIgnoreCase)) + { + handle = RegisteredTextures[i]; + return true; + } + } + } + + int width = 512; + int height = 512; + byte[] dummyBytes = new byte[width * height * 4]; + Texture dummyTexture = new Texture(width, height, PixelFormat.RGBAlpha, dummyBytes, null); + dummyTexture.VideoContext = new VideoTexture(cleanPath, width, height, queryString); + dummyTexture.AvailableToUnload = true; + + int videoIdx = GetNextFreeTexture(); + RegisteredTextures[videoIdx] = dummyTexture; + RegisteredTexturesCount++; + handle = RegisteredTextures[videoIdx]; + return true; + } /* BUG: * Attempt to delete null texture handles from the end of the array * These sometimes seem to end up there @@ -203,6 +255,45 @@ public bool LoadTexture(ref Texture handle, OpenGlTextureWrapMode wrap, int curr { return false; } + + if (handle.VideoContext != null) + { + VideoTexture video = handle.VideoContext as VideoTexture; + if (video != null) + { + if (!handle.OpenGlTextures[(int)wrap].Valid) + { + int[] names = new int[1]; + GL.GenTextures(1, names); + GL.BindTexture(TextureTarget.Texture2D, names[0]); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (float)TextureMinFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (float)TextureMagFilter.Linear); + if ((wrap & OpenGlTextureWrapMode.RepeatClamp) != 0) + { + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (float)TextureWrapMode.Repeat); + } + else + { + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (float)TextureWrapMode.ClampToEdge); + } + + if ((wrap & OpenGlTextureWrapMode.ClampRepeat) != 0) + { + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (float)TextureWrapMode.Repeat); + } + else + { + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (float)TextureWrapMode.ClampToEdge); + } + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, video.Width, video.Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero); + handle.OpenGlTextures[(int)wrap].Name = names[0]; + handle.OpenGlTextures[(int)wrap].Valid = true; + video.Initialize(); + } + video.Render(handle.OpenGlTextures[(int)wrap].Name, currentTicks); + return true; + } + } if (handle.MultipleFrames) { @@ -479,6 +570,13 @@ public static void UnloadTexture(ref Texture handle) return; } + if (handle.VideoContext != null) + { + IDisposable video = handle.VideoContext as IDisposable; + video?.Dispose(); + handle.VideoContext = null; + } + if (handle.MultipleFrames) { for (int i = 0; i < handle.TotalFrames; i++) @@ -587,9 +685,20 @@ public void UnloadUnusedTextures(double TimeElapsed) { for (int i = 0; i < RegisteredTextures.Length; i++) { - if (RegisteredTextures[i] != null && RegisteredTextures[i].AvailableToUnload && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 20000) + if (RegisteredTextures[i] != null) { - UnloadTexture(ref RegisteredTextures[i]); + if (RegisteredTextures[i].VideoContext != null) + { + VideoTexture video = RegisteredTextures[i].VideoContext as VideoTexture; + if (video != null && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 100) + { + video.Pause(); + } + } + if (RegisteredTextures[i].AvailableToUnload && (CPreciseTimer.GetClockTicks() - RegisteredTextures[i].LastAccess) > 20000) + { + UnloadTexture(ref RegisteredTextures[i]); + } } } } diff --git a/source/LibRender2/Textures/VideoTexture.cs b/source/LibRender2/Textures/VideoTexture.cs new file mode 100644 index 000000000..81253343d --- /dev/null +++ b/source/LibRender2/Textures/VideoTexture.cs @@ -0,0 +1,357 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using OpenBveApi.Textures; +using OpenTK.Graphics.OpenGL; + +namespace LibRender2.Textures +{ + public class VideoTexture : IDisposable + { + public string VideoPath { get; private set; } + public int Width { get; private set; } + public int Height { get; private set; } + public int TextureId { get; private set; } + + private IntPtr mpvHandle; + private IntPtr renderContext; + private int fboId; + private bool initialized; + private bool shouldResize; + private bool resolutionResolved; + private VideoApi.GetProcAddressCallback getProcAddressDelegate; + + private string loopOption = "inf"; + private bool isPaused; + + public void Pause() + { + if (initialized && !isPaused) + { + VideoApi.mpv_set_option_string(mpvHandle, "pause", "yes"); + isPaused = true; + } + } + + public void Resume() + { + if (initialized && isPaused) + { + VideoApi.mpv_set_option_string(mpvHandle, "pause", "no"); + isPaused = false; + } + } + + public VideoTexture(string videoPath, int width = 512, int height = 512, string queryString = "") + { + VideoPath = videoPath; + Width = width; + Height = height; + + if (!string.IsNullOrEmpty(queryString)) + { + string[] pairs = queryString.Split('&'); + foreach (string pair in pairs) + { + string[] kv = pair.Split('='); + if (kv.Length == 2) + { + string key = kv[0].Trim().ToLowerInvariant(); + string val = kv[1].Trim().ToLowerInvariant(); + if (key == "loop") + { + if (val == "no" || val == "false" || val == "0") + { + loopOption = "no"; + } + else + { + loopOption = val; + } + } + } + } + } + } + + public void Initialize() + { + if (initialized) return; + + try + { + mpvHandle = VideoApi.mpv_create(); + if (mpvHandle == IntPtr.Zero) return; + + // Loop video and optimize caching + VideoApi.mpv_set_option_string(mpvHandle, "loop-file", loopOption); + VideoApi.mpv_set_option_string(mpvHandle, "keep-open", "yes"); + VideoApi.mpv_set_option_string(mpvHandle, "hwdec", "auto-safe"); + VideoApi.mpv_set_option_string(mpvHandle, "vo", "libmpv"); + VideoApi.mpv_set_option_string(mpvHandle, "ytdl", "no"); + VideoApi.mpv_set_option_string(mpvHandle, "load-scripts", "no"); + VideoApi.mpv_set_option_string(mpvHandle, "aid", "no"); + VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-skiploopfilter", "all"); + VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-fast", "yes"); + VideoApi.mpv_set_option_string(mpvHandle, "vd-lavc-threads", "2"); + VideoApi.mpv_set_option_string(mpvHandle, "demuxer-max-bytes", "10485760"); + VideoApi.mpv_set_option_string(mpvHandle, "demuxer-max-back-bytes", "0"); + + if (VideoApi.mpv_initialize(mpvHandle) < 0) + { + Dispose(); + return; + } + + // Setup OpenGL context sharing + getProcAddressDelegate = VideoApi.GetProcAddressOpenGL; + IntPtr getProcAddressPtr = Marshal.GetFunctionPointerForDelegate(getProcAddressDelegate); + + VideoApi.mpv_opengl_init_params glInitParams = new VideoApi.mpv_opengl_init_params + { + get_proc_address = getProcAddressPtr, + get_proc_address_ctx = IntPtr.Zero, + extra_fields = IntPtr.Zero + }; + + IntPtr glInitParamsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(glInitParams)); + Marshal.StructureToPtr(glInitParams, glInitParamsPtr, false); + + VideoApi.mpv_render_param[] renderParams = new VideoApi.mpv_render_param[] + { + new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_API_TYPE, data = Marshal.StringToHGlobalAnsi(VideoApi.MPV_RENDER_API_TYPE_OPENGL) }, + new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data = glInitParamsPtr }, + new VideoApi.mpv_render_param { type = VideoApi.MPV_RENDER_PARAM_TYPE_INVALID, data = IntPtr.Zero } + }; + + if (VideoApi.mpv_render_context_create(out renderContext, mpvHandle, renderParams) < 0) + { + Marshal.FreeHGlobal(renderParams[0].data); + Marshal.FreeHGlobal(glInitParamsPtr); + Dispose(); + return; + } + + Marshal.FreeHGlobal(renderParams[0].data); + Marshal.FreeHGlobal(glInitParamsPtr); + + // Load video + string commandString = "loadfile"; + IntPtr cmdStringPtr = Marshal.StringToHGlobalAnsi(commandString); + IntPtr pathPtr = Marshal.StringToHGlobalAnsi(VideoPath); + IntPtr[] args = new IntPtr[] { cmdStringPtr, pathPtr, IntPtr.Zero }; + VideoApi.mpv_command(mpvHandle, args); + Marshal.FreeHGlobal(cmdStringPtr); + Marshal.FreeHGlobal(pathPtr); + + initialized = true; + } + catch + { + Dispose(); + } + } + + private void UpdateResolution() + { + if (mpvHandle == IntPtr.Zero || resolutionResolved) return; + + IntPtr wPtr = VideoApi.mpv_get_property_string(mpvHandle, "video-params/w"); + IntPtr hPtr = VideoApi.mpv_get_property_string(mpvHandle, "video-params/h"); + + if (wPtr != IntPtr.Zero && hPtr != IntPtr.Zero) + { + string wStr = Marshal.PtrToStringAnsi(wPtr); + string hStr = Marshal.PtrToStringAnsi(hPtr); + + if (int.TryParse(wStr, out int w) && int.TryParse(hStr, out int h)) + { + if (w > 0 && h > 0) + { + Width = w; + Height = h; + shouldResize = true; + resolutionResolved = true; + } + } + } + + if (wPtr != IntPtr.Zero) VideoApi.mpv_free(wPtr); + if (hPtr != IntPtr.Zero) VideoApi.mpv_free(hPtr); + } + + private int lastRenderTicks = -1; + + public void Render(int glTextureId, int currentTicks) + { + if (!initialized) return; + Resume(); + + // Limit video rendering to ~30 FPS (33ms) to prevent FPS drops + int elapsed = currentTicks - lastRenderTicks; + if (lastRenderTicks != -1 && elapsed >= 0 && elapsed < 33) return; + + // Check if new frame is actually available to render + ulong flags = VideoApi.mpv_render_context_update(renderContext); + if ((flags & 1) == 0) // MPV_RENDER_UPDATE_FRAME = 1 << 0 + { + return; + } + lastRenderTicks = currentTicks; + + try + { + TextureId = glTextureId; + + UpdateResolution(); + + if (shouldResize) + { + GL.BindTexture(TextureTarget.Texture2D, glTextureId); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, Width, Height, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero); + shouldResize = false; + } + + if (fboId == 0) + { + GL.GenFramebuffers(1, out fboId); + } + + // Backup OpenGL state + int prevFbo; + GL.GetInteger(GetPName.FramebufferBinding, out prevFbo); + + int prevVAO, prevVBO, prevEBO, prevProgram, prevTexture, prevActiveTexture; + GL.GetInteger(GetPName.VertexArrayBinding, out prevVAO); + GL.GetInteger(GetPName.ArrayBufferBinding, out prevVBO); + GL.GetInteger(GetPName.ElementArrayBufferBinding, out prevEBO); + GL.GetInteger(GetPName.CurrentProgram, out prevProgram); + GL.GetInteger(GetPName.TextureBinding2D, out prevTexture); + GL.GetInteger(GetPName.ActiveTexture, out prevActiveTexture); + + int[] prevViewport = new int[4]; + GL.GetInteger(GetPName.Viewport, prevViewport); + + bool prevScissor = GL.IsEnabled(EnableCap.ScissorTest); + int[] prevScissorBox = new int[4]; + GL.GetInteger(GetPName.ScissorBox, prevScissorBox); + + bool prevDepth = GL.IsEnabled(EnableCap.DepthTest); + bool prevBlend = GL.IsEnabled(EnableCap.Blend); + bool prevCull = GL.IsEnabled(EnableCap.CullFace); + + // Backup write masks + bool[] prevColorMask = new bool[4]; + GL.GetBoolean(GetPName.ColorWritemask, prevColorMask); + bool prevDepthMask; + GL.GetBoolean(GetPName.DepthWritemask, out prevDepthMask); + + // Backup stencil test + bool prevStencil = GL.IsEnabled(EnableCap.StencilTest); + + // Backup blend equation and functions + int prevBlendSrcRgb, prevBlendDstRgb, prevBlendSrcAlpha, prevBlendDstAlpha; + GL.GetInteger(GetPName.BlendSrcRgb, out prevBlendSrcRgb); + GL.GetInteger(GetPName.BlendDstRgb, out prevBlendDstRgb); + GL.GetInteger(GetPName.BlendSrcAlpha, out prevBlendSrcAlpha); + GL.GetInteger(GetPName.BlendDstAlpha, out prevBlendDstAlpha); + int prevBlendEqRgb, prevBlendEqAlpha; + GL.GetInteger(GetPName.BlendEquationRgb, out prevBlendEqRgb); + GL.GetInteger(GetPName.BlendEquationAlpha, out prevBlendEqAlpha); + + // Backup clear color + float[] prevClearColor = new float[4]; + GL.GetFloat(GetPName.ColorClearValue, prevClearColor); + + // Bind FBO and texture + GL.BindFramebuffer(FramebufferTarget.Framebuffer, fboId); + GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, TextureId, 0); + + // Set viewport for FBO size + GL.Viewport(0, 0, Width, Height); + GL.Disable(EnableCap.ScissorTest); + + mpv_opengl_fbo fboParam = new mpv_opengl_fbo + { + fbo = fboId, + w = Width, + h = Height, + internal_format = 0x8058 // GL_RGBA8 + }; + + IntPtr fboParamPtr = Marshal.AllocHGlobal(Marshal.SizeOf(fboParam)); + Marshal.StructureToPtr(fboParam, fboParamPtr, false); + + VideoApi.mpv_render_param[] renderParams = new VideoApi.mpv_render_param[] + { + new VideoApi.mpv_render_param { type = 3, data = fboParamPtr }, // MPV_RENDER_PARAM_OPENGL_FBO = 3 + new VideoApi.mpv_render_param { type = 0, data = IntPtr.Zero } + }; + + VideoApi.mpv_render_context_render(renderContext, renderParams); + + Marshal.FreeHGlobal(fboParamPtr); + + // Restore OpenGL state + GL.BindFramebuffer(FramebufferTarget.Framebuffer, prevFbo); + GL.UseProgram(prevProgram); + GL.BindVertexArray(prevVAO); + GL.BindBuffer(BufferTarget.ArrayBuffer, prevVBO); + GL.BindBuffer(BufferTarget.ElementArrayBuffer, prevEBO); + GL.ActiveTexture((TextureUnit)prevActiveTexture); + GL.BindTexture(TextureTarget.Texture2D, prevTexture); + + GL.Viewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); + if (prevScissor) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest); + GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]); + if (prevDepth) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest); + if (prevBlend) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend); + if (prevCull) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace); + + GL.ColorMask(prevColorMask[0], prevColorMask[1], prevColorMask[2], prevColorMask[3]); + GL.DepthMask(prevDepthMask); + if (prevStencil) GL.Enable(EnableCap.StencilTest); else GL.Disable(EnableCap.StencilTest); + + // Safely restore blend functions and equations + GL.BlendFuncSeparate((BlendingFactorSrc)prevBlendSrcRgb, (BlendingFactorDest)prevBlendDstRgb, (BlendingFactorSrc)prevBlendSrcAlpha, (BlendingFactorDest)prevBlendDstAlpha); + GL.BlendEquationSeparate((BlendEquationMode)prevBlendEqRgb, (BlendEquationMode)prevBlendEqAlpha); + + // Restore clear color + GL.ClearColor(prevClearColor[0], prevClearColor[1], prevClearColor[2], prevClearColor[3]); + } + catch + { + // Ignore render errors to avoid crash + } + } + + public void Dispose() + { + initialized = false; + if (renderContext != IntPtr.Zero) + { + VideoApi.mpv_render_context_free(renderContext); + renderContext = IntPtr.Zero; + } + if (mpvHandle != IntPtr.Zero) + { + VideoApi.mpv_terminate_destroy(mpvHandle); + mpvHandle = IntPtr.Zero; + } + if (fboId != 0) + { + GL.DeleteFramebuffers(1, ref fboId); + fboId = 0; + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct mpv_opengl_fbo + { + public int fbo; + public int w; + public int h; + public int internal_format; + } +} diff --git a/source/OpenBveApi/OpenBveApi.csproj b/source/OpenBveApi/OpenBveApi.csproj index c75e05d79..5514098af 100644 --- a/source/OpenBveApi/OpenBveApi.csproj +++ b/source/OpenBveApi/OpenBveApi.csproj @@ -302,6 +302,7 @@ + Textures.cs diff --git a/source/OpenBveApi/Textures/Textures.cs b/source/OpenBveApi/Textures/Textures.cs index 6e029c0f5..3e5713d46 100644 --- a/source/OpenBveApi/Textures/Textures.cs +++ b/source/OpenBveApi/Textures/Textures.cs @@ -43,6 +43,8 @@ public class Texture public int TotalFrames; /// Whether this texture uses the compatible transparency mode (Matches to the nearest color in a restricted palette) public bool CompatibleTransparencyMode; + /// Holds the video context reference + public object VideoContext; /// Gets the color of the given pixel /// The pixel index diff --git a/source/OpenBveApi/Textures/VideoApi.cs b/source/OpenBveApi/Textures/VideoApi.cs new file mode 100644 index 000000000..3cf54b894 --- /dev/null +++ b/source/OpenBveApi/Textures/VideoApi.cs @@ -0,0 +1,192 @@ +using System; +using System.Runtime.InteropServices; + +namespace OpenBveApi.Textures +{ + /// Provides API bindings for libmpv video playback. + public static class VideoApi + { + private const string MpvDll = "libmpv-2.dll"; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr LoadLibraryW(string lpLibFileName); + + [DllImport("libdl", EntryPoint = "dlopen")] + private static extern IntPtr dlopen(string filename, int flags); + + static VideoApi() + { + try + { + string arch = IntPtr.Size == 8 ? "x64" : "x86"; + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + bool isWindows = System.IO.Path.DirectorySeparatorChar == '\\'; + + if (isWindows) + { + string path = System.IO.Path.Combine(baseDir, arch, MpvDll); + if (System.IO.File.Exists(path)) + { + LoadLibraryW(path); + } + } + else + { + // Unix/macOS: Map to .so or .dylib + string libName = "libmpv.so"; + if (System.IO.File.Exists(System.IO.Path.Combine(baseDir, arch, "libmpv.dylib"))) + { + libName = "libmpv.dylib"; + } + string path = System.IO.Path.Combine(baseDir, arch, libName); + if (System.IO.File.Exists(path)) + { + dlopen(path, 2); // RTLD_NOW = 2 + } + } + } + catch + { + } + } + + /// Creates an mpv instance. + /// A pointer to the mpv handle. + [DllImport(MpvDll, EntryPoint = "mpv_create", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr mpv_create(); + + /// Initializes the mpv instance. + /// The mpv handle. + /// An integer status code. + [DllImport(MpvDll, EntryPoint = "mpv_initialize", CallingConvention = CallingConvention.Cdecl)] + public static extern int mpv_initialize(IntPtr handle); + + /// Destroys and terminates the mpv instance. + /// The mpv handle. + [DllImport(MpvDll, EntryPoint = "mpv_terminate_destroy", CallingConvention = CallingConvention.Cdecl)] + public static extern void mpv_terminate_destroy(IntPtr handle); + + /// Executes a command on the mpv instance. + /// The mpv handle. + /// The command arguments. + /// An integer status code. + [DllImport(MpvDll, EntryPoint = "mpv_command", CallingConvention = CallingConvention.Cdecl)] + public static extern int mpv_command(IntPtr handle, IntPtr[] args); + + /// Sets an option string on the mpv instance. + /// The mpv handle. + /// The option name. + /// The option value. + /// An integer status code. + [DllImport(MpvDll, EntryPoint = "mpv_set_option_string", CallingConvention = CallingConvention.Cdecl)] + public static extern int mpv_set_option_string(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value); + + /// Gets a property value from the mpv instance. + /// The mpv handle. + /// The property name. + /// A pointer to the property value string. + [DllImport(MpvDll, EntryPoint = "mpv_get_property_string", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr mpv_get_property_string(IntPtr handle, [MarshalAs(UnmanagedType.LPStr)] string name); + + /// Frees data allocated by mpv. + /// The pointer to the data to free. + [DllImport(MpvDll, EntryPoint = "mpv_free", CallingConvention = CallingConvention.Cdecl)] + public static extern void mpv_free(IntPtr data); + + /// Invalid render parameter type. + public const int MPV_RENDER_PARAM_TYPE_INVALID = 0; + /// API type render parameter. + public const int MPV_RENDER_PARAM_API_TYPE = 1; + /// OpenGL init parameters render parameter. + public const int MPV_RENDER_PARAM_OPENGL_INIT_PARAMS = 2; + + /// OpenGL render API type string. + public const string MPV_RENDER_API_TYPE_OPENGL = "opengl"; + + /// Represents an mpv render parameter. + [StructLayout(LayoutKind.Sequential)] + public struct mpv_render_param + { + /// The parameter type. + public int type; + /// The parameter data pointer. + public IntPtr data; + } + + /// Represents OpenGL initialization parameters for mpv. + [StructLayout(LayoutKind.Sequential)] + public struct mpv_opengl_init_params + { + /// Pointer to the get_proc_address callback. + public IntPtr get_proc_address; + /// Pointer to the callback context. + public IntPtr get_proc_address_ctx; + /// Pointer to extra fields. + public IntPtr extra_fields; + } + + /// Delegate callback to retrieve OpenGL procedure addresses. + /// The callback context. + /// The procedure name. + /// The address of the procedure. + public delegate IntPtr GetProcAddressCallback(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string name); + + /// Creates an mpv render context. + /// The resulting render context pointer. + /// The mpv instance handle. + /// The parameter list. + /// An integer status code. + [DllImport(MpvDll, EntryPoint = "mpv_render_context_create", CallingConvention = CallingConvention.Cdecl)] + public static extern int mpv_render_context_create(out IntPtr res, IntPtr mpv, mpv_render_param[] params_list); + + /// Frees the mpv render context. + /// The render context pointer. + [DllImport(MpvDll, EntryPoint = "mpv_render_context_free", CallingConvention = CallingConvention.Cdecl)] + public static extern void mpv_render_context_free(IntPtr ctx); + + /// Renders a frame using the context. + /// The render context pointer. + /// The render parameter list. + /// An integer status code. + [DllImport(MpvDll, EntryPoint = "mpv_render_context_render", CallingConvention = CallingConvention.Cdecl)] + public static extern int mpv_render_context_render(IntPtr ctx, mpv_render_param[] params_list); + + /// Checks/updates the render context state. + /// The render context pointer. + /// Flags indicating update status. + [DllImport(MpvDll, EntryPoint = "mpv_render_context_update", CallingConvention = CallingConvention.Cdecl)] + public static extern ulong mpv_render_context_update(IntPtr ctx); + + // Helper to load DLL on Windows + [DllImport("opengl32.dll", CharSet = CharSet.Ansi)] + private static extern IntPtr wglGetProcAddress(string name); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi)] + private static extern IntPtr GetProcAddress(IntPtr hModule, string name); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetModuleHandle(string lpModuleName); + + /// Retrieves OpenGL procedure address on Windows. + /// The context pointer. + /// The procedure name. + /// The address of the procedure. + public static IntPtr GetProcAddressOpenGL(IntPtr ctx, string name) + { + try + { + IntPtr proc = wglGetProcAddress(name); + if (proc == IntPtr.Zero) + { + IntPtr hModule = GetModuleHandle("opengl32.dll"); + proc = GetProcAddress(hModule, name); + } + return proc; + } + catch + { + return IntPtr.Zero; + } + } + } +}