Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/headless.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ builder.UseDefaultRendering(new WindowConfig(Size: (1280, 720), Title: "Hotbar")

An agent usually needs no code change for this: `PIXELY_HEADLESS=1` switches any app to headless mode, see [Environment variables](#environment-variables).

`screenshot <path>` writes the last rendered frame as a PNG; the file exists once the command's frame has run. A path that cannot be written, or a screenshot before the first frame has rendered, throws out of the frame loop. Commands run before that frame's render, so a screenshot right after the commands it should show is one frame too early; put `wait 1;` before it. The capture waits for the GPU, so that frame takes longer.
`screenshot <path>` writes the last rendered frame as a PNG; the file exists once the command's frame has run. Missing directories of the path are created. A path that still cannot be written, such as one whose parent is a file, throws out of the frame loop. So does a screenshot before the first frame has rendered. Commands run before that frame's render, so a screenshot right after the commands it should show is one frame too early; put `wait 1;` before it. The capture waits for the GPU, so that frame takes longer.

The frame is also available to code through `OffscreenWindow.CaptureLastFrame()`, and `IImageWriter` saves a tightly packed, non-planar, non-indexed `Image` as a PNG; `Build()` registers the SDL one unless the app registered its own.
The frame is also available to code through `OffscreenWindow.CaptureLastFrame()`, and `IImageWriter` saves a tightly packed, non-planar, non-indexed `Image` as a PNG, creating missing directories; `Build()` registers the SDL one unless the app registered its own.

### Environment variables

Expand Down
6 changes: 6 additions & 0 deletions src/Pixely/Content/SdlImageWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public unsafe void SavePng(Image image, string path)
throw new NotSupportedException($"Only tightly packed images in a non-planar, non-indexed pixel format can be saved, not {image.PixelFormat} with {pixels.Length} bytes for {width}x{height}.");
}

string? directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (directory != null)
{
Directory.CreateDirectory(directory);
}

fixed (byte* pixelsPointer = pixels)
{
Pointer<SDL_Surface> surface = SDL3.SDL_CreateSurfaceFrom(width, height, format, (IntPtr)pixelsPointer, pitch);
Expand Down
42 changes: 42 additions & 0 deletions tests/Pixely.Tests/SdlImageWriterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Pixely.Content;
using Pixely.Gpu;

namespace Pixely.Tests;

public class SdlImageWriterTests
{
private DirectoryInfo _temporaryDirectory = null!;

[SetUp]
public void SetUp()
{
_temporaryDirectory = Directory.CreateTempSubdirectory("Pixely.Tests-");
}

[TearDown]
public void TearDown()
{
_temporaryDirectory.Delete(recursive: true);
}

[Test]
public void SavePng_CreatesMissingDirectories()
{
string path = Path.Combine(_temporaryDirectory.FullName, "frames", "run", "shot.png");

new SdlImageWriter().SavePng(CreateImage(), path);

Assert.That(File.Exists(path), Is.True);
}

[Test]
public void SavePng_WithParentThatIsAFile_Throws()
{
string parent = Path.Combine(_temporaryDirectory.FullName, "frames");
File.WriteAllBytes(parent, []);

Assert.That(() => new SdlImageWriter().SavePng(CreateImage(), Path.Combine(parent, "shot.png")), Throws.InstanceOf<IOException>());
}

private static RawImage CreateImage() => new(new byte[2 * 2 * 4], new ShortSize(2, 2), PixelFormat.Rgba8888);
}