Jump to content

Recommended Posts

Posted

Started by looking into new shader families and inspecting shader output layout.
Default shader layout is defined in "Shaders/Base/Fragment.glsl"

layout(location = 0) out vec4 outColor[8];

Is it right that values from "Shaders/PBR/Fragment.glsl" outColor[attachmentindex] can be accessed if you attach a texture array to color attachment 0 and define appropriate RenderFlags uniform? Seems great, less collision opportunities with custom shader layouts!

The example code from Camera::SetRenderTarget gives an artifact, although the only moving object is a cam2, it feels as if all 3 objects in the scene are getting rotated and casting shadows to the red ball.
UPD: Ok, it's because cone and sphere object also have the same material as the white box, not a bug.

 

 

Posted

@Josh 
Here comes an example of using multiple color attachments in texture buffer.
It almost works out the box, which is super great! Please check the notes at the very end.

You can create a new shader family inside the editor:
Project > + > New Shader Family

Shader layout is declared inside Shaders/Base/Fragment.glsl

layout(location = 0) out vec4 outColor[8];

By default, PRB shader outputs additional information to the texture buffer color attachment with index in [1, 7] based on the 'RenderFlags' value
see: Shaders/PBR/Fragment.glsl
see: outColor[attachmentindex]

In our custom user-hook-shader we can output something by our own choice, e.g. the surface normal obtained from the PBR shader:

...
void UserHook(inout Surface surface, in Material material)
{
    outColor[1] = vec4(surface.normal.xyz, 1.0);
...

C++ code:

#include "Leadwerks.h"

using namespace Leadwerks;

int main(int argc, const char* argv[])
{
  auto displays    = GetDisplays();
  auto window      = CreateWindow("Ultra Engine", 0, 0, 1280, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
  auto world       = CreateWorld();
  auto framebuffer = CreateFramebuffer(window);
  auto cam1        = CreateCamera(world);

  cam1->SetClearColor(0.125);
  cam1->SetPosition(0, 0, -3);
  cam1->SetFov(70);

  //Create scenery
  auto light = CreateBoxLight(world);
  light->SetRange(-10, 10);
  light->SetRotation(15, 15, 0);
  light->SetColor(2);

  auto box1 = CreateBox(world);
  auto box2 = CreateBox(world);
  auto cone = CreateCone(world);
  auto sphr = CreateSphere(world);

  box1->SetColor(1, 1, 1);
  box2->SetColor(1, 1, 1);
  cone->SetColor(0, 0, 1);
  sphr->SetColor(1, 0, 0);

  box1->SetPosition( 0.00, +0.50, 0.00);
  box2->SetPosition( 0.00, -0.50, 0.00);
  cone->SetPosition(+1.25,  0.00, 0.00);
  sphr->SetPosition(-1.25,  0.00, 0.00);

  //Create render target with texture buffer which has 2 color attachment textures
  auto texbuffer = CreateTextureBuffer(256, 256, 2, true, 0);
  auto texbase   = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  auto texnorm   = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer->SetColorAttachment(texbase, 0);
  texbuffer->SetColorAttachment(texnorm, 1);

  //Create camera with render target attached
  auto cam2 = CreateCamera(world);
  cam2->SetClearColor(1, 1, 1);
  cam2->SetRenderTarget(texbuffer);

  //Configure render layers to render
  //1. cone and shpere only to cam2
  //2. boxes only to cam1
  cam1->SetRenderLayers(0b01);
  box1->SetRenderLayers(0b01);
  box2->SetRenderLayers(0b01);

  cam2->SetRenderLayers(0b10);
  cone->SetRenderLayers(0b10);
  sphr->SetRenderLayers(0b10);

  //Create render target material with custom shader family which has 2 color outputs
  auto shfcust = LoadShaderFamily("Shaders/Example-TextureBuffer-SetColorAttachment.fam");
  auto mtlcust = CreateMaterial();
  mtlcust->SetShaderFamily(shfcust);

  //Create render target debug output fragment base color material
  auto mtlbase = CreateMaterial();
  mtlbase->SetTexture(texbase);

  //Create render target debug output fragment normal material
  auto mtlnorm = CreateMaterial();
  mtlnorm->SetTexture(texnorm);

  //Apply custom material to cone and sphere
  cone->SetMaterial(mtlcust);
  sphr->SetMaterial(mtlcust);

  //Apply render target debug output materials to boxes
  box1->SetMaterial(mtlbase);
  box2->SetMaterial(mtlnorm);

  //Main loop
  while (window->Closed() == false and window->KeyDown(KEY_ESCAPE) == false)
  {
    //Orient the texturebuffer camera
    cam2->SetPosition(0, 0, 0);
    cam2->Turn(0, 1, 0);
    cam2->Move(0, 0, -3);

    world->Update();
    world->Render(framebuffer);
  }
  return 0;
}

Result:
image.thumb.png.53c645d86b0d574a9213093a7bb42a41.png

NOTES:

  1. I had to add #include "../Base/Fragment.glsl" inside custom Fragment.glsl to have access to outColor
  2. I had to add #include guard inside /Base/Fragment.glsl to avoid duplicate declarations with PBR/Fragment.glsl
  3. Example didn't work with texbuffer->GetColorAttachment(1) without explicitly setting texbuffer->SetColorAttachment(texnorm, 1)
    May be a bug or some misunderstanding.
  4. My initial though to add "layout(location = 1) out vec4 outCustom;" didn't work.
    Is it because the overlap with the default "layout(location = 0) out vec4 outColor[8];"?
  5. Added #ifndef USER_HOOK section to avoid overwriting outColor[attachmentindex] in PBR/Fragment.glsl, but I'm not sure if it's actually needed.
  6. I notice some kind of a stall on application close
Posted

Hmmmm, this is interesting. The number of color attachments will vary if the camera uses post-processing effects, or refraction, or SSR. But if you are not adding any post-processing effect to that camera I suppose it would be okay to just output to all those color attachments in one shader.

My job is to make tools you love, with the features you want, and performance you can't live without.

  • 2 months later...
Posted

I'm back with the 'minimal' cubemap texture render target sample which I was able to get using the engine. Not actually a cubemap, but I suppose an actual cubemap is only supported for the depth component still. Will try out the depth attachment cubemap as a next step.
Any news on the non-postprocess material/shader uniform variables?

image.png.62acfebead4f6c4d44885d361cc8ac9a.png

#include "Leadwerks.h"

using namespace Leadwerks;

int main(int argc, const char* argv[])
{
  auto displays    = GetDisplays();
  auto window      = CreateWindow("Ultra Engine", 0, 0, 720, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
  auto world       = CreateWorld();
  auto framebuffer = CreateFramebuffer(window);

  // Create 6 planes to act as a cube debug output
  auto plane_0 = CreatePlane(world);
  auto plane_1 = CreatePlane(world);
  auto plane_2 = CreatePlane(world);
  auto plane_3 = CreatePlane(world);
  auto plane_4 = CreatePlane(world);
  auto plane_5 = CreatePlane(world);

  plane_0->SetColor(1.f);
  plane_1->SetColor(1.f);
  plane_2->SetColor(1.f);
  plane_3->SetColor(1.f);
  plane_4->SetColor(1.f);
  plane_5->SetColor(1.f);

  plane_0->AlignToVector(+1.f,  0.f,  0.f, 1, 1.f, +90.f);
  plane_1->AlignToVector(-1.f,  0.f,  0.f, 1, 1.f, -90.f);
  plane_2->AlignToVector( 0.f, +1.f,  0.f, 1, 1.f, -90.f);
  plane_3->AlignToVector( 0.f, -1.f,  0.f, 1, 1.f, +90.f);
  plane_4->AlignToVector( 0.f,  0.f, +1.f, 1, 1.f, -90.f);
  plane_5->AlignToVector( 0.f,  0.f, -1.f, 1, 1.f, -90.f);

  plane_0->SetPosition(+0.5f,  0.0f,  0.0f, true);
  plane_1->SetPosition(-0.5f,  0.0f,  0.0f, true);
  plane_2->SetPosition( 0.0f, +0.5f,  0.0f, true);
  plane_3->SetPosition( 0.0f, -0.5f,  0.0f, true);
  plane_4->SetPosition( 0.0f,  0.0f, +0.5f, true);
  plane_5->SetPosition( 0.0f,  0.0f, -0.5f, true);

  // Create scene
  auto cone_0 = CreateCone(world);
  auto sphr_0 = CreateSphere(world);
  auto cube_0 = CreateBox(world);
  auto cone_1 = CreateCone(world);
  auto sphr_1 = CreateSphere(world);
  auto cube_1 = CreateBox(world);

  cone_0->SetColor(0.f, 0.f, 1.f);
  sphr_0->SetColor(1.f, 0.f, 0.f);
  cube_0->SetColor(0.f, 1.f, 0.f);
  cone_1->SetColor(1.f, 0.f, 1.f);
  sphr_1->SetColor(1.f, 1.f, 0.f);
  cube_1->SetColor(0.f, 1.f, 1.f);

  cone_0->SetPosition( 0.f, +1.f, +2.f);
  sphr_0->SetPosition(+2.f, +1.f,  0.f);
  cube_0->SetPosition(+1.f, +2.f, +1.f);
  cone_1->SetPosition( 0.f, -1.f, -2.f);
  sphr_1->SetPosition(-2.f, -1.f,  0.f);
  cube_1->SetPosition(-1.f, -2.f, -1.f);

  // Create camera to render into window
  auto cam_main = CreateCamera(world);
  cam_main->SetClearColor(0.125f);
  cam_main->SetFov(60.f);

  // Create cubemap render targets
  auto texbuffer_0 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_0 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_0->SetColorAttachment(texbase_0, 0);

  auto texbuffer_1 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_1 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_1->SetColorAttachment(texbase_1, 0);

  auto texbuffer_2 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_2 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_2->SetColorAttachment(texbase_2, 0);

  auto texbuffer_3 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_3 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_3->SetColorAttachment(texbase_3, 0);

  auto texbuffer_4 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_4 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_4->SetColorAttachment(texbase_4, 0);

  auto texbuffer_5 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbase_5 = CreateTexture(TextureType::TEXTURE_2D, 256, 256);
  texbuffer_5->SetColorAttachment(texbase_5, 0);

  // Create and configure cubemap render target cameras
  auto cam_rt_0 = CreateCamera(world);
  auto cam_rt_1 = CreateCamera(world);
  auto cam_rt_2 = CreateCamera(world);
  auto cam_rt_3 = CreateCamera(world);
  auto cam_rt_4 = CreateCamera(world);
  auto cam_rt_5 = CreateCamera(world);

  cam_rt_0->AlignToVector(+1.f, 0.f, 0.f, 2);
  cam_rt_1->AlignToVector(-1.f, 0.f, 0.f, 2);
  cam_rt_2->AlignToVector(0.f, +1.f, 0.f, 2);
  cam_rt_3->AlignToVector(0.f, -1.f, 0.f, 2);
  cam_rt_4->AlignToVector(0.f, 0.f, +1.f, 2);
  cam_rt_5->AlignToVector(0.f, 0.f, -1.f, 2);

  cam_rt_0->SetClearColor(0.5f);
  cam_rt_1->SetClearColor(0.5f);
  cam_rt_2->SetClearColor(0.5f);
  cam_rt_3->SetClearColor(0.5f);
  cam_rt_4->SetClearColor(0.5f);
  cam_rt_5->SetClearColor(0.5f);

  cam_rt_0->SetFov(90.0);
  cam_rt_1->SetFov(90.0);
  cam_rt_2->SetFov(90.0);
  cam_rt_3->SetFov(90.0);
  cam_rt_4->SetFov(90.0);
  cam_rt_5->SetFov(90.0);

  cam_rt_0->SetOrder(0);
  cam_rt_1->SetOrder(0);
  cam_rt_2->SetOrder(0);
  cam_rt_3->SetOrder(0);
  cam_rt_4->SetOrder(0);
  cam_rt_5->SetOrder(0);
  cam_main->SetOrder(1);

  cam_rt_0->SetRenderTarget(texbuffer_0);
  cam_rt_1->SetRenderTarget(texbuffer_1);
  cam_rt_2->SetRenderTarget(texbuffer_2);
  cam_rt_3->SetRenderTarget(texbuffer_3);
  cam_rt_4->SetRenderTarget(texbuffer_4);
  cam_rt_5->SetRenderTarget(texbuffer_5);

  // Configure render layers
  cam_main->SetRenderLayers(0b01);
  plane_0->SetRenderLayers(0b01);
  plane_1->SetRenderLayers(0b01);
  plane_2->SetRenderLayers(0b01);
  plane_3->SetRenderLayers(0b01);
  plane_4->SetRenderLayers(0b01);
  plane_5->SetRenderLayers(0b01);
  cam_rt_0->SetRenderLayers(0b10);
  cam_rt_1->SetRenderLayers(0b10);
  cam_rt_2->SetRenderLayers(0b10);
  cam_rt_3->SetRenderLayers(0b10);
  cam_rt_4->SetRenderLayers(0b10);
  cam_rt_5->SetRenderLayers(0b10);
  cone_0->SetRenderLayers(0b11);
  sphr_0->SetRenderLayers(0b11);
  cube_0->SetRenderLayers(0b11);
  cone_1->SetRenderLayers(0b11);
  sphr_1->SetRenderLayers(0b11);
  cube_1->SetRenderLayers(0b11);

  // Create render target debug output base color material
  auto mtlbase_0 = CreateMaterial();
  auto mtlbase_1 = CreateMaterial();
  auto mtlbase_2 = CreateMaterial();
  auto mtlbase_3 = CreateMaterial();
  auto mtlbase_4 = CreateMaterial();
  auto mtlbase_5 = CreateMaterial();

  mtlbase_0->SetTexture(texbase_0);
  mtlbase_1->SetTexture(texbase_1);
  mtlbase_2->SetTexture(texbase_2);
  mtlbase_3->SetTexture(texbase_3);
  mtlbase_4->SetTexture(texbase_4);
  mtlbase_5->SetTexture(texbase_5);

  // Apply render target debug output materials to cube faces
  plane_0->SetMaterial(mtlbase_0);
  plane_1->SetMaterial(mtlbase_1);
  plane_2->SetMaterial(mtlbase_2);
  plane_3->SetMaterial(mtlbase_3);
  plane_4->SetMaterial(mtlbase_4);
  plane_5->SetMaterial(mtlbase_5);

  // Main loop
  while (window->Closed() == false and
         window->KeyDown(KEY_ESCAPE) == false)
  {
    cam_main->SetPosition(0.f, 0.f, 0.f);
    cam_main->Turn(1.f, 1.f,  0.f);
    cam_main->Move(0.f, 0.f, -3.f);

    world->Update();
    world->Render(framebuffer, true, 60);
  }
  return 0;
}
Posted

Quick follow-up: depth cubemap render target sample

image.png.95077516054173f6bb9d9ba8d875109f.png

 

#include "Leadwerks.h"

using namespace Leadwerks;

int main(int argc, const char* argv[])
{
  auto displays    = GetDisplays();
  auto window      = CreateWindow("Ultra Engine", 0, 0, 720, 720, displays[0], WINDOW_CENTER | WINDOW_TITLEBAR);
  auto world       = CreateWorld();
  auto framebuffer = CreateFramebuffer(window);

  // Create cube debug model output
  auto cube = CreateBox(world);
  cube->SetColor(1.f);

  // Create scene
  auto cone_0 = CreateCone(world);
  auto sphr_0 = CreateSphere(world);
  auto cube_0 = CreateBox(world);
  auto cone_1 = CreateCone(world);
  auto sphr_1 = CreateSphere(world);
  auto cube_1 = CreateBox(world);

  cone_0->SetColor(0.f, 0.f, 1.f);
  sphr_0->SetColor(1.f, 0.f, 0.f);
  cube_0->SetColor(0.f, 1.f, 0.f);
  cone_1->SetColor(1.f, 0.f, 1.f);
  sphr_1->SetColor(1.f, 1.f, 0.f);
  cube_1->SetColor(0.f, 1.f, 1.f);

  cone_0->SetPosition( 0.f, +1.f, +2.f);
  sphr_0->SetPosition(+2.f, +1.f,  0.f);
  cube_0->SetPosition(+1.f, +2.f, +1.f);
  cone_1->SetPosition( 0.f, -1.f, -2.f);
  sphr_1->SetPosition(-2.f, -1.f,  0.f);
  cube_1->SetPosition(-1.f, -2.f, -1.f);

  // Create camera to render into window
  auto cam_main = CreateCamera(world);
  cam_main->SetClearColor(0.125f);
  cam_main->SetFov(60.f);

  // Create cubemap render targets
  auto texbuffer_0 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbuffer_1 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbuffer_2 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbuffer_3 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbuffer_4 = CreateTextureBuffer(256, 256, 1, true, 0);
  auto texbuffer_5 = CreateTextureBuffer(256, 256, 1, true, 0);

  auto texdepth = CreateTexture(TextureType::TEXTURE_CUBE, 256, 256, TextureFormat::TEXTURE_DEPTH, {}, 6);
  texbuffer_0->SetDepthAttachment(texdepth, 0);
  texbuffer_1->SetDepthAttachment(texdepth, 1);
  texbuffer_2->SetDepthAttachment(texdepth, 2);
  texbuffer_3->SetDepthAttachment(texdepth, 3);
  texbuffer_4->SetDepthAttachment(texdepth, 4);
  texbuffer_5->SetDepthAttachment(texdepth, 5);

  // Create and configure cubemap render target cameras
  auto cam_rt_0 = CreateCamera(world);
  auto cam_rt_1 = CreateCamera(world);
  auto cam_rt_2 = CreateCamera(world);
  auto cam_rt_3 = CreateCamera(world);
  auto cam_rt_4 = CreateCamera(world);
  auto cam_rt_5 = CreateCamera(world);

  cam_rt_0->AlignToVector(+1.f, 0.f, 0.f, 2);
  cam_rt_1->AlignToVector(-1.f, 0.f, 0.f, 2);
  cam_rt_2->AlignToVector(0.f, +1.f, 0.f, 2, 1.f, +90.f);
  cam_rt_3->AlignToVector(0.f, -1.f, 0.f, 2, 1.f, +90.f);
  cam_rt_4->AlignToVector(0.f, 0.f, +1.f, 2);
  cam_rt_5->AlignToVector(0.f, 0.f, -1.f, 2);

  cam_rt_0->SetClearColor(0.0f);
  cam_rt_1->SetClearColor(0.0f);
  cam_rt_2->SetClearColor(0.0f);
  cam_rt_3->SetClearColor(0.0f);
  cam_rt_4->SetClearColor(0.0f);
  cam_rt_5->SetClearColor(0.0f);

  cam_rt_0->SetFov(90.0);
  cam_rt_1->SetFov(90.0);
  cam_rt_2->SetFov(90.0);
  cam_rt_3->SetFov(90.0);
  cam_rt_4->SetFov(90.0);
  cam_rt_5->SetFov(90.0);

  cam_rt_0->SetOrder(0);
  cam_rt_1->SetOrder(0);
  cam_rt_2->SetOrder(0);
  cam_rt_3->SetOrder(0);
  cam_rt_4->SetOrder(0);
  cam_rt_5->SetOrder(0);
  cam_main->SetOrder(1);

  cam_rt_0->SetRenderTarget(texbuffer_0);
  cam_rt_1->SetRenderTarget(texbuffer_1);
  cam_rt_2->SetRenderTarget(texbuffer_2);
  cam_rt_3->SetRenderTarget(texbuffer_3);
  cam_rt_4->SetRenderTarget(texbuffer_4);
  cam_rt_5->SetRenderTarget(texbuffer_5);

  // Configure render layers
  cam_main->SetRenderLayers(0b01);
  cube->SetRenderLayers(0b01);
  cam_rt_0->SetRenderLayers(0b10);
  cam_rt_1->SetRenderLayers(0b10);
  cam_rt_2->SetRenderLayers(0b10);
  cam_rt_3->SetRenderLayers(0b10);
  cam_rt_4->SetRenderLayers(0b10);
  cam_rt_5->SetRenderLayers(0b10);
  cone_0->SetRenderLayers(0b11);
  sphr_0->SetRenderLayers(0b11);
  cube_0->SetRenderLayers(0b11);
  cone_1->SetRenderLayers(0b11);
  sphr_1->SetRenderLayers(0b11);
  cube_1->SetRenderLayers(0b11);

  // Create render target debug output depth material
  auto mtldepth = CreateMaterial();
  auto shdcbmp  = LoadShaderFamily("Shaders/Example-TextureBuffer-SetDepthAttachment-Cubemap.fam");
  mtldepth->SetShaderFamily(shdcbmp);
  mtldepth->SetTexture(texdepth);

  // Apply render target debug output material
  cube->SetMaterial(mtldepth);

  // Main loop
  while (window->Closed() == false and
         window->KeyDown(KEY_ESCAPE) == false)
  {
    cam_main->SetPosition(0.f, 0.f, 0.f);
    cam_main->Turn(1.f, 1.f,  0.f);
    cam_main->Move(0.f, 0.f, -3.f);

    world->Update();
    world->Render(framebuffer, true, 60);
  }
  return 0;
}

Example-TextureBuffer-SetDepthAttachment-Cubemap/Fragment.glsl:

...
    //-----------------------------------------------------------------------------------------
    // Base texture
    //-----------------------------------------------------------------------------------------

    if (material.textureHandle[TEXTURE_BASE] != uvec2(0))
    {
        vec3 texCoords = normalize(vertexWorldPosition.xyz);
        surface.basecolor.rgb = vec3(pow(textureLod(samplerCube(material.textureHandle[TEXTURE_BASE]), texCoords, 0).r, 10.0f));
    }
...
Posted
On 5/7/2025 at 10:24 AM, Vladimir Sabantsev said:

Any news on the non-postprocess material/shader uniform variables?

The engine uses merged batches (see id Software's stuff) so it's not really possible to assign extra per-material uniforms as the scene is being drawn, because the renderer draws all materials at once. The easiest way to do this is to pack the values into a shader and then use texelFetch() to read a value from the texture using precise integer coordinates. The other alternative would be a per-shader SetUniform method, which I already use in the renderer, but it would affect every surface that uses that shader, so I am not sure if that's very useful.

My job is to make tools you love, with the features you want, and performance you can't live without.

Posted
8 hours ago, Josh said:

The engine uses merged batches (see id Software's stuff) so it's not really possible to assign extra per-material uniforms as the scene is being drawn, because the renderer draws all materials at once. The easiest way to do this is to pack the values into a shader and then use texelFetch() to read a value from the texture using precise integer coordinates.

Okay, then it's still possible to pass anything you want per-material. The only question is how much of a performance drawback is it to overwrite texture pixels from the code?
 

8 hours ago, Josh said:

The other alternative would be a per-shader SetUniform method, which I already use in the renderer, but it would affect every surface that uses that shader, so I am not sure if that's very useful.

Fits like a glove for my use case. I will need access to the position of the cubemap depth camera from the shader. Not a doubt that it will also come handy for other custom shaders in some way.

Posted
4 hours ago, Vladimir Sabantsev said:

Okay, then it's still possible to pass anything you want per-material. The only question is how much of a performance drawback is it to overwrite texture pixels from the code?

It would not be much. I mean, whether it's a small texture or a uniform, you are sending data to the GPU either way.

Create a texture using format TEXTURE_RGBA32, call SetPixels() each frame, then access the camera position in the shader like this:

vec3 pos = texelFetch(sampler2D(material.textureHandles[5]), ivec2(0,0), 0).xyz;

 

My job is to make tools you love, with the features you want, and performance you can't live without.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...