Add a new curve type for elliptical arcs
and use it for rounded rectangles and circles.
We use the 'E' command to represent elliptical
arcs in serialized paths.
The magical term to know about (because the GLSL compiler or the
validation layers sure as hell don't) is:
"dynamically uniform expression"
because if you don't have that when indexing a texture or buffer array,
you need to add nonuniformEXT() around the index variable.
Fixes the close icon on AMD having glitches of the previous icon visible
in some pixels.
We must be careful with single-point contours
that contain just a move. These never occur in
practice, but our randomized tests produce them
regularly.
Based on reverse engineering the color node and contrary to my
expectations, the matrix/offset is expressed in, and applied to,
unpremultiplied colors. The colors are being explicitly
unpremultiplied, transformed according to the matrix/offset, and
premultiplied back (see color_matrix.glsl). The matrix is getting
transposed.
Also, copy the same blurb to the corresponding GtkSnapshot function.
Signed-off-by: Sergey Bugaev <bugaevc@gmail.com>
The (out caller-allocates) and (out callee-allocates) annotations are
meant for structured or pointer types. Plain old data types are just
regular out parameters and don't need the annotation about who allocates
them.
See glib!2005, gjs#570
Take a rendernode as source and a GskPath and GskStroke,
and fill the area that is covered when stroking the path
with the given stroke parameters, like cairo_stroke() would.
This commit adds the basic infrastructure for paths.
The public APIs consists of GskPath, GskPathPoint and
GskPathBuilder.
GskPath is a data structure for paths that consists
of contours, which in turn might contain Bézier curves.
The Bezier data structure is inspired by Skia, with separate
arrays for points and operations. One advantage of this
arrangement is that start and end points are shared
between adjacent curves.
A GskPathPoint represents a point on a path, which can
be queried for various properties.
GskPathBuilder is an auxiliary builder object for paths.
graphene_rect_t is not well-suited for this purpose,
since you end up with floating-point precision problems
at the upper bound (x + width, y + height).
Instead of scale and whatnot, pass:
1. The image size
2. The viewport to map to that image size
and compute everything else from there.
In particular, we set the Vulkan viewport to the image dimensions
instead of the viewport size.
All of this makes things a lot simpler while keeping the required
functionality.
We need them for mask-only textures.
For tiffs, we convert the formats to RGBA (the idea that tiff can save
everything needs to be buried I guess) as tiffs can't do alpha-only.
Add a bunch of inline functions for graphene_rectangle_t.
We use those quite extensively in tight loops so making them as fast as
possible via inlining has massive benefits.
The current render-heavy benchmark I am playing (th paris-30k in node-editor)
went from 49fps to 85fps on my AMD.
When a GdkMemoryFormat is not supported natively and there's
postprocessing required, add a way to mark a VulkanImage as such via the
new postprocess flags.
Also allow texting such iamges only with new_for_upload() and detect
when that is the case and then run a postprocessing step that converts
that image to a suitable format.
This is done with a new "convert" shader/op.
This now supports all formats natively, no conversions happen on the CPU
anymore (unless the GPU is old).
Add an explicit begin() and an end() op. For now, this looks like
overkill, but it allows doing renderpasses with custom ops that are not
meant to render a rendernode.
Examples for this are pre/postprocessing passes or 2-pass blur.
The API was using regions because it always had. But all the code ever
did was get the extents of the region.
So simplify everything by using rectangles everywhere.
These days, we can query it with gsk_vulkan_render_get_context().
Makes quite a few functions require one less argument.
And it also makes the GskVulkanRenderPass empty. Gotta figure out what
to do with it.
Instead, build-depnd on glslc to build them.
glslc is available in all important distros for a while:
Fedora >= 28
Ubuntu >= 23.04
Debian >= 12
Arch
Opensuse >= 15.2
msys2
are the ones I checked.
So we can depend on it and avoid having to deal with keeping spirv files
up-to-date in all commits.
It's also 700kB of data, and not updating it helps.
We now store all the relevant state of the image inside the VulkanImage
struct, so we can delay barriers for as long as possible.
Whenever we want to use an image, we call the new
gsk_vulkan_image_transition() and it will add a barrier to the desired
state if one is necessary.
... and all the remaining functions still using it.
It's all unused and has been replaced by upload and download ops.
With this change, all GPU operations now go via GskVulkanOp.command()
and no more side channels exist.
This op queues a download of an image. The image will only be available
once the commands finished executing, so it requires waiting for the
render to finish, which makes the API a bit awkward.
Included is also a download_png_op() useful for debugging.
The render pass ops were not updating the image's layout to the final
layout when a render pass ends.
Fix that.
Also make the layouts explicit arguments to the render pass op.
Split out the function that uploads using a buffer, so that it can be
used with an area to only update parts of the image.
That feature is not used yet, but will be in future commits.
If a command takes too long to execute, Vulkan drivers will think they
are inflooping and abort what they were doing.
For the simple color shader with smallish nodes, this happens around
10M instances, as tested with the output of
./tests/rendernode-create-tests 10000000 colors.node
So just limit it to way lower, so that we barely never hit it, ut still
pick a big number so this optimization stays noticable.
For small regions, the optimization doesn't matter that much, so we
don't need to do lots of work on the CPU.
In particular, this should catch icons and their backgrounds (32x32),
but I was generous in selecting the number.
Gets my discrete AMD on widget-factory back to the 1900fps it had before
this optimization while making the driver clock the GPU's shader at
1.7GHz instead of the 2.1GHz it used before.
Using clear avoids the shader engine (see last commit), so if we can get
pixels out of it, we should.
So we detect the overlap with the rounded corners of the clip region and
emit shaders for those, but then use Clear() for the rest.
With this in place, widget-factory on my integrated Intel TigerLake gets
a 60% performance boost.
The op emits a vkCmdClearAttachments() with a given color. That can be
used with color nodes that are pixel-aligned and opaque to significantly
speed up rendering when the window background is a solid color.
However, currently this fails a bit outside of fullscreen when rounded
clip rectangles are in use to draw rounded corners.
Instead of using the upload vfunc and going via the code in
GskVulkanImage, copy/paste the relevant code into the command() vfunc.
This is meant to achieve multiple things:
1. Get rid of GskVulkanUploader and its own command buffer and general
non-integration with operations.
2. Get rid of GskVulkanOp:upload()
3. Get the upload/download code machinery for GskVulkanImage and put it
with the actual operations.
The current code can't do direct upload/download, that will follow in a
future commit.
... instead of doing the equivalent things manually by creating a
RenderPass and calling the relevant functions.
Now all renderpass operations are indeed stored in ops.
Also reshuffle the command emission code, because we no longer need to
emit the ops for the base renderpass.
As a result we only submit a single command buffer containing all the
render passes instead of once per render pass.
We also bind vertex buffers and descriptor sets only once now at the
start instead of once per renderpass.
Use the OpClass.stage to order operations:
1. Put upload ops first
This way we can ensure they are executed first.
2. Move subpasses for offscreens in front of the pass using them.
This is a massive refactoring because it collects all the renderops
of all renderpasses into one long array in the Render object.
Lots of code in there is still flaky and needs cleanup. That will
follow in further commits.
Other than that it does work fine though.
All the ops that just execute a shader do pretty much the same stuff, so
put it all in a single function that they all call.
It's basically faking a base class for them.
Instead of recreating the same renderpass object in every frame and for
every offscreen, just reuse it.
Technically, we can save this per-renderer or even per-display (it
should really be cached by VkDevice), but we have no infrastructure for
that.
The function name gsk_vulkan_render_get_pipeline() had been used for
GskVulkanPipeline. Since those are gone now, we can use that name for
VkPipelines.
Renderpasses get recreated every frame, but we keep render objects
around. So if we keep the vertex buffer in the render object, we can
also keep it around and just reuse it.
Also, we only need one buffer for all the render passes, which is
another bonus.
The initial buffer size is chosen at 128kB. Maximized Nautilus,
gnome-text-editor with an open file and widget-factory take ~100kB when
doing a full redraw. Other apps are between 30-50kB usually.
So I chose a value that is not too big, but catches ~90% of cases.
Interning strings is slow, especially if we can instead do direct
pointer compares.
Also refactor the pipeline lookup code a bit to make use of the
refactored code.
Set it after creating all the ops and then use it for iterating.
Note that we cannot set it while creating the ops because the array may
be realloc()ed into a different memory region which would invalidate all
the pointers.
It currently has no use, but that will come later.
Also put the typedefs into headers in gsk/vulkan, they have nthing to do
outside that directory.
Remove the function to add a node from both the GskVulkanRender and the
GskVulkanRenderPass.
That means they are both now meant to draw exactly one node.
This is a rudimentary - but working - port.
Glyph uploads are still using the old machinery, a bunch of functions
still exist that probably aren't necessary anymore and each glyph emits
its own node.
This will need to be improved in further commits.
This shader is an updated version of the mask shader, but I want to use
the mask name for the mask node and that's a different functionality.
Also, add an operation for it and partially implement the mask node
using it, so we can test that this shader works.
Replacing the shader used for text rendering is the next step.