diff --git a/LagFreeScreenshots/API/LfsApi.cs b/LagFreeScreenshots/API/LfsApi.cs index 23b0426..9776421 100644 --- a/LagFreeScreenshots/API/LfsApi.cs +++ b/LagFreeScreenshots/API/LfsApi.cs @@ -5,7 +5,7 @@ namespace LagFreeScreenshots.API public static class LfsApi { /// - /// Called after a creenshot is taken and written to disk + /// Called after a screenshot is taken and written to disk /// public static event ScreenshotSavedEventV2? OnScreenshotSavedV2; @@ -13,5 +13,15 @@ public static class LfsApi internal static void InvokeScreenshotSaved(string filePath, int width, int height, MetadataV2? metadataV2) => OnScreenshotSavedV2?.Invoke(filePath, width, height, metadataV2); + + + /// + /// Called just after the camera rendered to this RenderTexture (just before it will be destroyed) + /// It's right time to make a GPU copy with Graphics.CopyTexture for example + /// + public static event ScreenshotTextureEvent? OnScreenshotTexture; + public delegate void ScreenshotTextureEvent(UnityEngine.RenderTexture texture); + internal static void InvokeScreenshotTexture(UnityEngine.RenderTexture texture) => + OnScreenshotTexture?.Invoke(texture); } } \ No newline at end of file diff --git a/LagFreeScreenshots/LagFreeScreenshotsMod.cs b/LagFreeScreenshots/LagFreeScreenshotsMod.cs index 71aa466..c974ed5 100644 --- a/LagFreeScreenshots/LagFreeScreenshotsMod.cs +++ b/LagFreeScreenshots/LagFreeScreenshotsMod.cs @@ -214,28 +214,63 @@ public static async Task TakeScreenshot(Camera camera, int w, int h, bool hasAlp { await TaskUtilities.YieldToFrameEnd(); - // var renderTexture = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 8); - var renderTexture = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); - var maxMsaa = MaxMsaaCount(w, h); - renderTexture.antiAliasing = maxMsaa; - var oldCameraTarget = camera.targetTexture; var oldCameraFov = camera.fieldOfView; var oldAllowMsaa = camera.allowMSAA; var oldGraphicsMsaa = QualitySettings.antiAliasing; + // make screenshot upside up by rotating camera just for the rendering + var t = camera.transform; + Quaternion? camOrigRot = null; + ScreenshotRotation shotRotation = ScreenshotRotation.AutoRotationDisabled; + if (ourAutorotation.Value) + { + shotRotation = GetPictureAutorotation(camera); + var inverseAngle = shotRotation switch + { + // we need to also compensate for upside-down texture rendering (later below) + ScreenshotRotation.CounterClockwise90 => 90, + ScreenshotRotation.Clockwise90 => -90, + ScreenshotRotation.NoRotation => 180, + _ => 0, + }; + if (inverseAngle != 0) + { + camOrigRot = t.rotation; + t.rotation *= Quaternion.AngleAxis(inverseAngle, Vector3.forward); // inverse rotation + } + + // for some rotation, we have to swap width / height + if (shotRotation == ScreenshotRotation.Clockwise90 || shotRotation == ScreenshotRotation.CounterClockwise90) + { + // we have to swap FOV to preserve the original viewport + camera.fieldOfView = Camera.VerticalToHorizontalFieldOfView(camera.fieldOfView, h * 1f / w); + // and rotate the resolution + (w, h) = (h, w); + } + } + + // var renderTexture = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, 8); + var renderTexture = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); + var maxMsaa = MaxMsaaCount(w, h); + renderTexture.antiAliasing = maxMsaa; + camera.targetTexture = renderTexture; camera.allowMSAA = maxMsaa > 1; QualitySettings.antiAliasing = maxMsaa; camera.Render(); + if (camOrigRot != null) + t.rotation = camOrigRot.Value; // restore + camera.targetTexture = oldCameraTarget; camera.fieldOfView = oldCameraFov; camera.allowMSAA = oldAllowMsaa; QualitySettings.antiAliasing = oldGraphicsMsaa; renderTexture.ResolveAntiAliasedSurface(); + LfsApi.InvokeScreenshotTexture(renderTexture); (IntPtr, int) data = default; var readbackSupported = SystemInfo.supportsAsyncGPUReadback; @@ -329,53 +364,6 @@ private static ImageCodecInfo GetEncoder(ImageFormat format) return null; } - private static unsafe (IntPtr, int) TransposeAndDestroyOriginal((IntPtr, int Length) data, int w, int h, int step) - { - (IntPtr, int) newData = (Marshal.AllocHGlobal(data.Length), data.Length); - - byte* pixels = (byte*) data.Item1; - byte* newPixels = (byte*) newData.Item1; - for (var x = 0; x < w; x++) - for (var y = 0; y < h; y++) - for (var s = 0; s < step; s++) - newPixels[s + y * step + x * h * step] = pixels[s + x * step + y * w * step]; - - Marshal.FreeHGlobal(data.Item1); - return newData; - } - - private static unsafe void FlipVertInPlace((IntPtr, int Length) data, int w, int h, int step) - { - byte* pixels = (byte*) data.Item1; - for (var y = 0; y < h / 2; y++) - { - for (var x = 0; x < w * step; x++) - { - var t = pixels[x + y * w * step]; - pixels[x + y * w * step] = pixels[x + (h - y - 1) * w * step]; - pixels[x + (h - y - 1) * w * step] = t; - } - } - } - - private static unsafe void FlipHorInPlace((IntPtr, int Length) data, int w, int h, int step) - { - byte* pixels = (byte*) data.Item1; - for (var x = 0; x < w / 2; x++) - { - for (var y = 0; y < h; y++) - { - for (var s = 0; s < step; s++) - { - var t = pixels[s + x * step + y * w * step]; - pixels[s + x * step + y * w * step] = pixels[s + (w - x - 1) * step + y * w * step]; - pixels[s + (w - x - 1) * step + y * w * step] = t; - } - } - } - } - - private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Length) pixelsPair, int w, int h, bool hasAlpha, ScreenshotRotation rotationQuarters, MetadataV2 metadata) { @@ -387,61 +375,40 @@ private static async Task EncodeAndSavePicture(string filePath, (IntPtr, int Len if (Thread.CurrentThread == ourMainThread) MelonLogger.Error("Image encode is executed on main thread - it's a bug!"); - var step = hasAlpha ? 4 : 3; - - unsafe - { - // swap colors [a]rgb -> bgr[a] - byte* pixels = (byte*) pixelsPair.Item1; - for (int i = 0; i < pixelsPair.Length; i += step) - { - var t = pixels[i]; - pixels[i] = pixels[i + step - 1]; - pixels[i + step - 1] = t; - if (step != 4) continue; - - t = pixels[i + 1]; - pixels[i + 1] = pixels[i + step - 2]; - pixels[i + step - 2] = t; - } - } - - if (rotationQuarters == ScreenshotRotation.Clockwise90) //90deg cw - { - pixelsPair = TransposeAndDestroyOriginal(pixelsPair, w, h, step); - var t = w; - w = h; - h = t; - } - else if (rotationQuarters == ScreenshotRotation.Clockwise180) //180deg cw - { - FlipHorInPlace(pixelsPair, w, h, step); - } - else if (rotationQuarters == ScreenshotRotation.CounterClockwise90) //270deg cw - { - FlipHorInPlace(pixelsPair, w, h, step); - FlipVertInPlace(pixelsPair, w, h, step); - pixelsPair = TransposeAndDestroyOriginal(pixelsPair, w, h, step); - var t = w; w = h; h = t; - } - else - { - FlipVertInPlace(pixelsPair, w, h, step); - } - - var pixelFormat = hasAlpha ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb; - var format = ourFormat.Value == "auto" ? (hasAlpha ? "png" : "jpeg") : ourFormat.Value; using var bitmap = new Bitmap(w, h, pixelFormat); var bitmapData = bitmap.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.WriteOnly, pixelFormat); unsafe { - Buffer.MemoryCopy((void*) pixelsPair.Item1, (void*) bitmapData.Scan0, pixelsPair.Length, pixelsPair.Length); + // swap colors [a]rgb -> bgr[a] AND horizontal flip (unity render is 0,0 bottom left) + if (hasAlpha) // 32 bits + { + uint* input = (uint*)pixelsPair.Item1; + uint* output = (uint*)bitmapData.Scan0 + w - 1; // scan X-axis backward + for (uint y = 0; y < h; ++y, output += 2*w) + for (uint x = 0; x < w; ++x, ++input, --output) + { + uint v = *input; + // flip bit order (endianness) + *output = ((v >> 24) & 0xff) | ((v >> 8) & 0xff00) | ((v << 8) & 0xff0000) | ((v << 24) & 0xff000000); + } + } + else { // 24 bits + Int24Bits* input = (Int24Bits*)pixelsPair.Item1; + Int24Bits* output = (Int24Bits*)bitmapData.Scan0 + w - 1; // scan X-axis backward + for (uint y = 0; y < h; ++y, output += 2*w) + for (uint x = 0; x < w; ++x, ++input, --output) + { + // flip bit order (endianness) + (output->r, output->g, output->b) = (input->b, input->g, input->r); + } + } } bitmap.UnlockBits(bitmapData); Marshal.FreeHGlobal(pixelsPair.Item1); + var format = ourFormat.Value == "auto" ? (hasAlpha ? "png" : "jpeg") : ourFormat.Value; var description = metadata?.ToString(); // https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-constant-property-item-descriptions @@ -519,4 +486,12 @@ static string GetPath(int w, int h) return ourOurGetPathMethod(w, h); } } + + [StructLayout(LayoutKind.Sequential, Pack = 0)] + internal struct Int24Bits + { + public byte r; + public byte g; + public byte b; + } }