From d7ce74a3f7f941ffba5a9fa73b0d7d559897d6e7 Mon Sep 17 00:00:00 2001 From: Tim Foley Date: Mon, 7 Dec 2020 09:29:37 -0800 Subject: "Shader Toy" example and related fixes (#1629) * "Shader Toy" example and related fixes This change introduces a new `shader-toy` example program that is primarily designed to show how Slang's features for type-based encapsulation and modularity can be applied to modularity for effects along the lines of those from `shadertoy.com`. The Example ----------- The example is being checked in with an example "toy" effect that I hastily put together, so that it would not be encumbered with any IP concerns. I wrote the effect using the shadertoy.com editor, so I can be sure it is valid GLSL. During bringup of the application I used a pre-existing and larger effect for testing, so some of the support code that was added is not being used at present. The big-picture idea here is to have an exmaple that shows how to modularize things using Slang interfaces and generics, and then to use the Slang compiler API to manage the compilation, composition, specialization, and linking steps. For better or worse this leads to the sequence of API calls involved being much longer than what was in something like the `hello-world` example. Future Work (Example) --------------------- There is a lot of room for improvement and expansion here, so this should be viewed as a checkpoint of work in progress rather than something I'm claiming as a finalized demonstration of all we'd like to achieve. Areas for future work include: * We need to copy the integration of "Dear, IMGUI" that was already done for the `model-viewer` example so that this example can have a UI. * Now that the compilation flow is broken into all these additional steps, it should be possible to have the application load multiple effects as distinct modules, and then provide a UI for switching between them. The chosen effect module would be used to specialize the top-level shader(s) before kernel generation. * The checked-in logic includes a compute shader that can execute an effect, but that hasn't been tested nor has it been wired up to any kind of UI. We should have a way to switch between multiple execution methods, with a goal of eventually including CPU execution. * The "GLSL compatibility" code needs a lot of improvements before it is likely to be usable for a nontrivial number of shaders. Some of that work is waiting on Slang compiler fixes, though. * We should consider allowing the individual "toy" effects to define their own uniform parameters and expose those via a UI and reflection. The catch in this case is not that this would be difficult to do, but that it would be a semantic change to how shader toy effects currently work. The Compiler Fixes ------------------ Doing this work exposed a few bugs in Slang, and this change includes fixes for the ones that were quick to address. We already had logic in `slang-check-shader.cpp` that was validating the entry points in a compile request - either by checking the explicitly-listed entry points, or by scanning for `[shader("...")]` attributes. The problem is that the routine that did that checking was not being invoked on all compiles. The logic that handled entry points was only being run for manual compiles using `SlangCompileRequest`, while anything using `import` or `loadModule` would ignore entry points. I refactored the relevant code into a subroutine that will be invoked in all compilation scenarios. There were already `TODO` comments in `SpecializedComponentType` which made the point about how a specialized entry point like `myShader` would need to properly show that it has dependencies on both the module that defines `myShader` *and* the module that defines `YourType`, while only the former was being handled at present. I went ahead and implemented the logic to scan the generic arguments for a specialized compoment type in order to determine what module(s) the arguments depend on (both type arguments and witness tables). With that change, using `IComponentType::link` on a specialized component will properly pull in the module(s) that the generic arguments come from. In `slang-ir-legalize-types.cpp` we could run into assertion failures in debug builds because of code trying to legalize layout `IRAttr`s for fields or parameters with types that need legalization. In practice it is safe to skip these layout attributes, because legalization of the fields/parameters they pertain to would result in creation of entirely new layout attributes, and the old ones would then be unreferenced. Future Work (Fixes) ------------------- There are other compiler bugs that this work exposed, but which this change does not address. These will need to be resolved as part of subsequent changes: * Slang allows for default-initialization of variables of a generic type. That is, given `` a user is allowed to declare `T x = {};` and the Slang front-end does not complain. Instead, this leads to an internal compiler error during IR lowering. * The Slang `__init()` feature probably needs to be upgraded to a properly supported feature, and we probably need a way to make implementing default-initialization an easy thing (e.g., any `struct` type that has initial-value expressions for all its fields should automatically and implicitly satsify an `init();` requirement declared in an interface) * Iniside an `__init()` definition, code has mutable access to members of the enclosing type, but for some reason the front-end is incorrectly treating `this` as immutable in those contexts. As a result you can write to `someField` but not `this.someField`. * User-defined operator overloads flat out don't work (which isn't surprising given that no clients have decided to use them yet, and we have no test coverage for them). This is actually due to the shadowing rules being used for lookup right now, so a fix for this issue is going to have far-reaching consequences around what overloads are visible where (and anything that impacts overload resolution is a big can of worms, including around performance). * fixup: test case had missing main function --- examples/shader-toy/README.md | 25 ++ examples/shader-toy/example-effect.slang | 93 +++++ examples/shader-toy/main.cpp | 585 +++++++++++++++++++++++++++++++ examples/shader-toy/shader-toy.slang | 348 ++++++++++++++++++ 4 files changed, 1051 insertions(+) create mode 100644 examples/shader-toy/README.md create mode 100644 examples/shader-toy/example-effect.slang create mode 100644 examples/shader-toy/main.cpp create mode 100644 examples/shader-toy/shader-toy.slang (limited to 'examples') diff --git a/examples/shader-toy/README.md b/examples/shader-toy/README.md new file mode 100644 index 000000000..16772157f --- /dev/null +++ b/examples/shader-toy/README.md @@ -0,0 +1,25 @@ +Slang "Shader Toy" Example +========================== + +This example shows how to use Slang's support for generics and interfaces to define shader effects as separately-compiled modules. +The effects in this case are based on the popular [Shader Toy](https://www.shadertoy.com/) site. + +Goals +----- + +The big-picture goals of this example is to define effects as separately-compiled modules of Slang code, and to also allow different methods of executing those effects (e.g., via vertex/fragment shaders or compute shaders) to be defined as modules. +Each module should be something the Slang compiler can compile and check independently. + +Combining modules (e.g., a particular shader effect with a particular execution method) should be accomplished using first-class operations supported by the Slang API, instead of by ad hoc preprocessing or pasting of strings. + +Approach +-------- + +The key idea here is to codify the rules for what a shader toy effect needs to provide in an interface (here called `IShaderToyImageShader`), and to use that interface as a contract when checking both implementers and users of that interface. + +Individual effects become `struct` types that declare conformance to `IShaderToyImageShader`; the compiler can thus issue error messages when a time fails to satisfy its requirements. +Execution methods become generic functions that abstract over any type that implements `IShaderToyImageShader`; the compiler can confirm that they only use operations that are guaranteed by the interface to be present. + +Composition thus consists of "plugging in" a type that implements the interface as a type argument of the generic function that implements an execution method. + +While the interfaces and modules that this example works with are relatively low in complexity, these same techniques can be applied to modularize more complex shader code without the need for preprocessor or metaprogramming tricks. diff --git a/examples/shader-toy/example-effect.slang b/examples/shader-toy/example-effect.slang new file mode 100644 index 000000000..966894a87 --- /dev/null +++ b/examples/shader-toy/example-effect.slang @@ -0,0 +1,93 @@ +// example-effect.slang + +// This file provides an example of how a shader +// toy effect can be compiled as a Slang module. +// +// Every effect will depend on the module that +// defines out shader toy infrastructure: +// +import shader_toy; + +// The `shader_toy` module defines the interface +// that each effect must implement, and our +// specific effect will be a type that implements +// the interface: +// +struct ExampleEffect : IShaderToyImageShader +{ + // Our goal is that we can mostly just copy-paste + // the code for an effect from shadertoy.com into + // this file, and have something that works. + // + // Due to limitations in compatibility between + // GLSL and Slang, that won't always work, but + // it still helps to note where the original + // GLSL code begins/ends. + + // Note: the verison of this file that is checked + // in uses a placeholder effect so that this file + // does not need to concern itself with the license + // terms of particular effects on shadertoy.com. + +// BEGIN GLSL + +float rand(float n) +{ + return fract(sin(n) * 43758.5453123); +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) +{ + float screenScale = length(iResolution.xy); + vec2 uv = fragCoord / screenScale; + + float frequency = 5.0f; + vec2 pos = (uv + iTime*vec2(0.25f, 0.0f)) * frequency; + + vec2 center = floor(pos + vec2(0.5)); + + float r0 = rand(center.x*3.0f + center.y*7.0f); + float r1 = rand(center.x*7.0f + center.y*13.0f); + float r2 = rand(center.x*13.0f + center.y*3.0f); + + float p = mix(0.0f, 4.0f, r0); + float f = mix(5.0f, 8.0f, r1); + + float a = 0.5f * (1.0f + cos(iTime*f + p)); + + float rad0 = mix(0.1, 0.4, r2); + float rad1 = mix(0.2, 0.9, r0); + + float radius = 0.5f*mix(rad0, rad1, a); + + vec2 delta = pos - center; + float distance = length(delta); + + fragColor.xyz = vec3(r0, r1, r2); + fragColor.w = 1.0f; + + if(distance > radius) fragColor.xyz = vec3(0.25f); +} + +// END GLSL + + // The GLSL logic for the effect above might have included + // "global" declarations (which become fields since things + // are wrapped in a `struct`) with initializer, and we + // need a way for the code that uses an effect like this + // to get an instance that has been properly initialized. + // + // Right now, the `IShaderToyImageShader` interface requires + // a factory function `getDefault()`, so we will implement + // that here. + // + static This getDefault() + { + // Note: this code does not need to be updated for different + // GLSL effects, since it will default-initialize whatever + // members the `This` types has. + // + This value = {}; + return value; + } +} diff --git a/examples/shader-toy/main.cpp b/examples/shader-toy/main.cpp new file mode 100644 index 000000000..1efc3c745 --- /dev/null +++ b/examples/shader-toy/main.cpp @@ -0,0 +1,585 @@ +// main.cpp + +// This file provides the application code for the `shader-toy` example. +// +// Much of the logic here is identical to the simpler `hello-world` example, +// so we will not spend time commenting those parts that are identical or +// nearly identical. Readers who want detailed comments on a simpler example +// using Slang should look there. + +// This example uses the Slang C/C++ API, alonmg with its optional type +// for managing COM-style reference-counted pointers. +// +#include +#include +using Slang::ComPtr; + +// This example uses a graphics API abstraction layer that is implemented inside +// the Slang codebase for use in our sample programs and test cases. Use of +// this layer is *not* required or assumed when using the Slang language, +// compiler, and API. +// +#include "gfx/render.h" +#include "gfx/d3d11/render-d3d11.h" +#include "gfx/window.h" +using namespace gfx; + +// In order to display a shader toy effect using rasterization-based shader +// execution we need to render a full-screen triangle. We will define a +// small helper type that defines the data for such a triangle. +// +struct FullScreenTriangle +{ + struct Vertex + { + float position[2]; + }; + + enum { kVertexCount = 3 }; + + static const Vertex kVertices[kVertexCount]; +}; +const FullScreenTriangle::Vertex FullScreenTriangle::kVertices[FullScreenTriangle::kVertexCount] = +{ + { { -1, -1 } }, + { { -1, 3 } }, + { { 3, -1 } }, +}; + +// The application itself will be encapsulated in a C++ `struct` type +// so that it can easily scope its state without use of global variables. +// +struct ShaderToyApp +{ + +// The uniform data used by the shader is defined here as a simple +// POD ("plain old data") type. +// +// Note: This type must match the declaration of `ShaderToyUniforms` +// in the file `shader-toy.slang`. +// +// An application could instead use a shared header file to define +// this type, or use Slang's reflection capabilities to allocate +// and set parameters at runtime. For this simple example we did +// the expedient thing of having distinct Slang and C++ declarations. +// +struct Uniforms +{ + float iMouse[4]; + float iResolution[2]; + float iTime; +}; + +// Many Slang API functions return detailed diagnostic information +// (error messages, warnings, etc.) as a "blob" of data, or return +// a null blob pointer instead if there were no issues. +// +// For convenience, we define a subroutine that will dump the information +// in a diagnostic blob if one is produced, and skip it otherwise. +// +void diagnoseIfNeeded(slang::IBlob* diagnosticsBlob) +{ + if( diagnosticsBlob != nullptr ) + { + reportError("%s", (const char*) diagnosticsBlob->getBufferPointer()); + } +} + +// The main interesting part of the host application code is where we +// load, compile, inspect, and compose the Slang shader code. +// +Result loadShaderProgram(gfx::Renderer* renderer, RefPtr& outShaderProgram) +{ + // The first step in interacting with the Slang API is to create a "global session," + // which represents an instance of the Slang API loaded from the library. + // + ComPtr slangGlobalSession; + SLANG_RETURN_ON_FAIL(slang_createGlobalSession(SLANG_API_VERSION, slangGlobalSession.writeRef())); + + // Next, we need to create a compilation session (`slang::ISession`) that will provide + // a scope to all the compilation and loading of code we do. + // + // In an application like this, which doesn't make use of preprocessor-based specialization, + // we can create a single session and use it for the duration of the application. + // One important service the session provides is re-use of modules that have already + // been compiled, so that if two Slang files `import` the same module, the compiler + // will only load and check that module once. + // + // When creating a session we need to tell it what code generation targets we may + // want code generated for. It is valid to have zero or more targets, but many + // applications will only want one, corresponding to the graphics API they plan to use. + // This application is currently hard-coded to use D3D11, so we set up for compilation + // to DX bytecode. + // + // Note: the `TargetDesc` can also be used to set things like optimization settings + // for each target, but this application doesn't care to set any of that stuff. + // + slang::TargetDesc targetDesc = {}; + targetDesc.format = SLANG_DXBC; + targetDesc.profile = spFindProfile(slangGlobalSession, "sm_4_0"); + + // The session can be set up with a few other options, notably: + // + // * Any search paths that should be used when resolving `import` or `#include` directives. + // + // * Any preprocessor macros to pre-define when reading in files. + // + // This application doesn't plan to make heavy use of the preprocessor, and all its + // shader files are in the same directory, so we just use the default options (which + // will lead to the only search path being the current working directory). + // + slang::SessionDesc sessionDesc = {}; + sessionDesc.targetCount = 1; + sessionDesc.targets = &targetDesc; + + ComPtr slangSession; + SLANG_RETURN_ON_FAIL(slangGlobalSession->createSession(sessionDesc, slangSession.writeRef())); + + // Once the session has been created, we can start loading code into it. + // + // The simplest way to load code is by calling `loadModule` with the name of a Slang + // module. A call to `loadModule("MyStuff")` will behave more or less as if you + // wrote: + // + // import MyStuff; + // + // In a Slang shader file. The compiler will use its search paths to try to locate + // `MyModule.slang`, then compile and load that file. If a matching module had + // already been loaded previously, that would be used directly. + // + // Note: The only interesting wrinkle here is that our file is named `shader-toy` with + // a hyphen in it, so the name is not directly usable as an identifier in Slang code. + // Instead, when trying to import this module in the context of Slang code, a user + // needs to replace the hyphens with underscores: + // + // import shader_toy; + // + ComPtr diagnosticsBlob; + slang::IModule* module = slangSession->loadModule("shader-toy", diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + if(!module) + return SLANG_FAIL; + + // Loading the `shader-toy` module will compile and check all the shader code in it, + // including the shader entry points we want to use. Now that the module is loaded + // we can look up those entry points by name. + // + // Note: If you are using this `loadModule` approach to load your shader code it is + // important to tag your entry point functions with the `[shader("...")]` attribute + // (e.g., `[shader("vertex")] void vertexMain(...)`). Without that information there + // is no umambiguous way for the compiler to know which functions represent entry + // points when it parses your code via `loadModule()`. + // + char const* vertexEntryPointName = "vertexMain"; + char const* fragmentEntryPointName = "fragmentMain"; + // + ComPtr vertexEntryPoint; + SLANG_RETURN_ON_FAIL(module->findEntryPointByName(vertexEntryPointName, vertexEntryPoint.writeRef())); + // + ComPtr fragmentEntryPoint; + SLANG_RETURN_ON_FAIL(module->findEntryPointByName(fragmentEntryPointName, fragmentEntryPoint.writeRef())); + + // At this point we have a few different Slang API objects that represent + // pieces of our code: `module`, `vertexEntryPoint`, and `fragmentEntryPoint`. + // + // A single Slang module could contain many different entry points (e.g., + // four vertex entry points, three fragment entry points, and two compute + // shaders), and before we try to generate output code for our target API + // we need to identify which entry points we plan to use together. + // + // Modules and entry points are both examples of *component types* in the + // Slang API. The API also provides a way to build a *composite* out of + // other pieces, and that is what we are going to do with our module + // and entry points. + // + List componentTypes; + componentTypes.add(module); + + // Later on when we go to extract compiled kernel code for our vertex + // and fragment shaders, we will need to make use of their order within + // the composition, so we will record the relative ordering of the entry + // points here as we add them. + int entryPointCount = 0; + int vertexEntryPointIndex = entryPointCount++; + componentTypes.add(vertexEntryPoint); + + int fragmentEntryPointIndex = entryPointCount++; + componentTypes.add(fragmentEntryPoint); + + // Actually creating the composite component type is a single operation + // on the Slang session, but the operation could potentially fail if + // something about the composite was invalid (e.g., you are trying to + // combine multiple copies of the same module), so we need to deal + // with the possibility of diagnostic output. + // + ComPtr composedProgram; + SlangResult result = slangSession->createCompositeComponentType( + componentTypes.getBuffer(), + componentTypes.getCount(), + composedProgram.writeRef(), + diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + SLANG_RETURN_ON_FAIL(result); + + // At this point, `composedProgram` represents the shader program + // we want to run, and the vertex and fragment shader there have + // been checked. + // + // We could use the Slang reflection API on `composedProgram` at this + // point to query things like the locations and offsets of the + // various uniform parameters, textures, etc. + // + // What *cannot* be done yet at this point is actually generating + // kernel code, because `composedProgram` includes a generic type + // parameter as part of the `fragmentMain` entry point: + // + // void fragmentMain(...) + // + // Our next task is to load code for a type we'd like to plug in + // for `T` there. + // + // Because Slang supports modular programming, there is no requirement + // that a type we want to plug in for `T` has to come from the + // same module, and to demonstrate that we will load a different + // module to provide the effect type we will plug in. + // + const char* effectModuleName = "example-effect"; + const char* effectTypeName = "ExampleEffect"; + slang::IModule* effectModule = slangSession->loadModule(effectModuleName, diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + if(!module) + return SLANG_FAIL; + + // Once we've loaded the code module that defines out effect type, + // we can look it up by name using the reflection information on + // the module. + // + // Note: A future version of the Slang API will support enumerating + // the types declared in a module so that we do not have to hard-code + // the name here. + // + auto effectType = effectModule->getLayout()->findTypeByName(effectTypeName); + + // Now that we have the `effectType` we want to plug in to our generic + // shader, we need to specialize the shader to that type. + // + // Because a shader program could have zero or more specialization parameters, + // we need to build up an array of specialization arguments. + // + List specializationArgs; + + { + // In our case, we only have a single specialization argument we plan + // to use, and it is a type argument. + // + slang::SpecializationArg effectTypeArg; + effectTypeArg.kind = slang::SpecializationArg::Kind::Type; + effectTypeArg.type = effectType; + specializationArgs.add(effectTypeArg); + } + + // Specialization of a component type is a single Slang API call, but + // we need to deal with the possibility of diagnostic output on failure. + // For example, if we tried to specialize the shader program to a + // type like `int` that doesn't support the `IShaderToyImageShader` interface, + // this is the step where we'd get an error message saying so. + // + ComPtr specializedProgram; + result = composedProgram->specialize( + specializationArgs.getBuffer(), + specializationArgs.getCount(), + specializedProgram.writeRef(), + diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + SLANG_RETURN_ON_FAIL(result); + + // At this point we have a specialized shader program that represents our + // intention to run the `vertexMain` and `fragmentMain` entry points, + // specialized to the `ExampleEffect` type we loaded. + // + // We can now *link* the program, which ensures that all of the code that + // it transitively depends on has been pulled together into a single + // component type. + // + ComPtr linkedProgram; + result = specializedProgram->link( + linkedProgram.writeRef(), + diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + SLANG_RETURN_ON_FAIL(result); + + // Given a linked program (one with no unresolved external references, + // and no not-yet-set specialization parameters), we can request kernel + // code for the entry points of that program by their indices. + // + // Because Slang supports a session with multiple active code generation + // targets, we also need to specify the index of the target we want + // code for, but since we have only a single target in this application, + // there isn't actually a choice. + // + int targetIndex = 0; + // + // Note: it is possible to get diagnostic messages when generating kernel + // code, but this is not a common occurence. Most semantic errors in + // user code will be detected at earlier steps in this compilation flow, + // but there are certain errors that are currently caught during final + // code generation (e.g., when using a function that is specific to one + // target, and then requesting kernel code for another target). + // + ComPtr vertexShaderBlob; + result = linkedProgram->getEntryPointCode(vertexEntryPointIndex, targetIndex, vertexShaderBlob.writeRef(), diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + SLANG_RETURN_ON_FAIL(result); + ComPtr fragmentShaderBlob; + result = linkedProgram->getEntryPointCode(fragmentEntryPointIndex, targetIndex, fragmentShaderBlob.writeRef(), diagnosticsBlob.writeRef()); + diagnoseIfNeeded(diagnosticsBlob); + SLANG_RETURN_ON_FAIL(result); + + // Once kernel code has been extracted from Slang, the rest of the logic + // here is basically the same as in the `hello-world` example. + + char const* vertexCode = (char const*) vertexShaderBlob->getBufferPointer(); + char const* vertexCodeEnd = vertexCode + vertexShaderBlob->getBufferSize(); + + char const* fragmentCode = (char const*) fragmentShaderBlob->getBufferPointer(); + char const* fragmentCodeEnd = fragmentCode + fragmentShaderBlob->getBufferSize(); + + gfx::ShaderProgram::KernelDesc kernelDescs[] = + { + { gfx::StageType::Vertex, vertexCode, vertexCodeEnd }, + { gfx::StageType::Fragment, fragmentCode, fragmentCodeEnd }, + }; + + gfx::ShaderProgram::Desc programDesc; + programDesc.pipelineType = gfx::PipelineType::Graphics; + programDesc.kernels = &kernelDescs[0]; + programDesc.kernelCount = 2; + + auto shaderProgram = renderer->createProgram(programDesc); + + outShaderProgram = shaderProgram; + return SLANG_OK; +} + +int gWindowWidth = 1024; +int gWindowHeight = 768; + +gfx::ApplicationContext* gAppContext; +gfx::Window* gWindow; +RefPtr gRenderer; +RefPtr gConstantBuffer; + +RefPtr gPipelineLayout; +RefPtr gPipelineState; +RefPtr gDescriptorSet; + +RefPtr gVertexBuffer; + +Result initialize() +{ + WindowDesc windowDesc; + windowDesc.title = "Slang Shader Toy"; + windowDesc.width = gWindowWidth; + windowDesc.height = gWindowHeight; + windowDesc.eventHandler = &_handleEvent; + windowDesc.userData = this; + gWindow = createWindow(windowDesc); + + gRenderer = createD3D11Renderer(); + Renderer::Desc rendererDesc; + rendererDesc.width = gWindowWidth; + rendererDesc.height = gWindowHeight; + { + Result res = gRenderer->initialize(rendererDesc, getPlatformWindowHandle(gWindow)); + if(SLANG_FAILED(res)) return res; + } + + int constantBufferSize = sizeof(Uniforms); + + BufferResource::Desc constantBufferDesc; + constantBufferDesc.init(constantBufferSize); + constantBufferDesc.setDefaults(Resource::Usage::ConstantBuffer); + constantBufferDesc.cpuAccessFlags = Resource::AccessFlag::Write; + + gConstantBuffer = gRenderer->createBufferResource( + Resource::Usage::ConstantBuffer, + constantBufferDesc); + if(!gConstantBuffer) return SLANG_FAIL; + + InputElementDesc inputElements[] = { + { "POSITION", 0, Format::RG_Float32, offsetof(FullScreenTriangle::Vertex, position) }, + }; + auto inputLayout = gRenderer->createInputLayout( + &inputElements[0], + SLANG_COUNT_OF(inputElements)); + if(!inputLayout) return SLANG_FAIL; + + BufferResource::Desc vertexBufferDesc; + vertexBufferDesc.init(FullScreenTriangle::kVertexCount * sizeof(FullScreenTriangle::Vertex)); + vertexBufferDesc.setDefaults(Resource::Usage::VertexBuffer); + gVertexBuffer = gRenderer->createBufferResource( + Resource::Usage::VertexBuffer, + vertexBufferDesc, + &FullScreenTriangle::kVertices[0]); + if(!gVertexBuffer) return SLANG_FAIL; + + RefPtr shaderProgram; + SLANG_RETURN_ON_FAIL(loadShaderProgram(gRenderer, shaderProgram)); + + DescriptorSetLayout::SlotRangeDesc slotRanges[] = + { + DescriptorSetLayout::SlotRangeDesc(DescriptorSlotType::UniformBuffer), + }; + DescriptorSetLayout::Desc descriptorSetLayoutDesc; + descriptorSetLayoutDesc.slotRangeCount = 1; + descriptorSetLayoutDesc.slotRanges = &slotRanges[0]; + auto descriptorSetLayout = gRenderer->createDescriptorSetLayout(descriptorSetLayoutDesc); + if(!descriptorSetLayout) return SLANG_FAIL; + + PipelineLayout::DescriptorSetDesc descriptorSets[] = + { + PipelineLayout::DescriptorSetDesc( descriptorSetLayout ), + }; + PipelineLayout::Desc pipelineLayoutDesc; + pipelineLayoutDesc.renderTargetCount = 1; + pipelineLayoutDesc.descriptorSetCount = 1; + pipelineLayoutDesc.descriptorSets = &descriptorSets[0]; + auto pipelineLayout = gRenderer->createPipelineLayout(pipelineLayoutDesc); + if(!pipelineLayout) return SLANG_FAIL; + + gPipelineLayout = pipelineLayout; + + auto descriptorSet = gRenderer->createDescriptorSet(descriptorSetLayout); + if(!descriptorSet) return SLANG_FAIL; + + descriptorSet->setConstantBuffer(0, 0, gConstantBuffer); + + gDescriptorSet = descriptorSet; + + GraphicsPipelineStateDesc desc; + desc.pipelineLayout = gPipelineLayout; + desc.inputLayout = inputLayout; + desc.program = shaderProgram; + desc.renderTargetCount = 1; + auto pipelineState = gRenderer->createGraphicsPipelineState(desc); + if(!pipelineState) return SLANG_FAIL; + + gPipelineState = pipelineState; + + showWindow(gWindow); + + return SLANG_OK; +} + +bool wasMouseDown = false; +bool isMouseDown = false; +float lastMouseX = 0.0f; +float lastMouseY = 0.0f; +float clickMouseX = 0.0f; +float clickMouseY = 0.0f; + +bool firstTime = true; +uint64_t startTime = 0; + +void renderFrame() +{ + if( firstTime ) + { + startTime = getCurrentTime(); + firstTime = false; + } + + static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 }; + gRenderer->setClearColor(kClearColor); + gRenderer->clearFrame(); + + if(Uniforms* uniforms = (Uniforms*) gRenderer->map(gConstantBuffer, MapFlavor::WriteDiscard)) + { + bool isMouseClick = isMouseDown && !wasMouseDown; + wasMouseDown = isMouseDown; + + if( isMouseClick ) + { + clickMouseX = lastMouseX; + clickMouseY = lastMouseY; + } + + uniforms->iMouse[0] = lastMouseX; + uniforms->iMouse[1] = lastMouseY; + uniforms->iMouse[2] = isMouseDown ? clickMouseX : -clickMouseX; + uniforms->iMouse[3] = isMouseClick ? clickMouseY : -clickMouseY; + uniforms->iTime = float( double(getCurrentTime() - startTime) / double(getTimerFrequency()) ); + uniforms->iResolution[0] = float(gWindowWidth); + uniforms->iResolution[1] = float(gWindowHeight); + + gRenderer->unmap(gConstantBuffer); + } + + gRenderer->setPipelineState(PipelineType::Graphics, gPipelineState); + gRenderer->setDescriptorSet(PipelineType::Graphics, gPipelineLayout, 0, gDescriptorSet); + + gRenderer->setVertexBuffer(0, gVertexBuffer, sizeof(FullScreenTriangle::Vertex)); + gRenderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); + + gRenderer->draw(3); + + gRenderer->presentFrame(); +} + +void finalize() +{ +} + +void handleEvent(Event const& event) +{ + switch( event.code ) + { + case EventCode::MouseDown: + isMouseDown = true; + lastMouseX = event.u.mouse.x; + lastMouseY = event.u.mouse.y; + break; + + case EventCode::MouseMoved: + lastMouseX = event.u.mouse.x; + lastMouseY = event.u.mouse.y; + break; + + case EventCode::MouseUp: + isMouseDown = false; + lastMouseX = event.u.mouse.x; + lastMouseY = event.u.mouse.y; + break; + + default: + break; + } +} + +static void _handleEvent(Event const& event) +{ + ShaderToyApp* app = (ShaderToyApp*) getUserData(event.window); + app->handleEvent(event); +} + + +}; + +void innerMain(ApplicationContext* context) +{ + ShaderToyApp app; + + if (SLANG_FAILED(app.initialize())) + { + return exitApplication(context, 1); + } + + while(dispatchEvents(context)) + { + app.renderFrame(); + } + + app.finalize(); +} + +GFX_UI_MAIN(innerMain) diff --git a/examples/shader-toy/shader-toy.slang b/examples/shader-toy/shader-toy.slang new file mode 100644 index 000000000..98583f246 --- /dev/null +++ b/examples/shader-toy/shader-toy.slang @@ -0,0 +1,348 @@ +// shader-toy.slang + +// This file implements the core of a system for executing +// code from shadertoy.com in the context of Slang. +// +// The big idea here is to define an interface so that +// different shader toy effects can be defined as +// separately compiled modules, and then "plug in" to +// execution environment for running those effects, wheter +// via vertex/fragment shaders, compute, or on CPU. +// +// An important goal is that we should be able to run effects +// defined on shadertoy.com with as little modification as +// possible. This goal isn't 100% achievable because shader +// toy effects are authored in GLSL, which differs from Slang +// in several ways, so this is an aspirational goal rather +// than a requirement. +// +// There are a few different kinds of effects supported +// by shader toy, which are enumerated on the [How To](https://www.shadertoy.com/howto) +// page of the project. This module focuses only on +// "image shaders," which are by far the most common. +// +// We will start with the interface that all image shaders +// are expected to implement. +// +interface IShaderToyImageShader +{ + // The shader toy "How To" page says: + // + // > Image shaders implement the `mainImage()` function in order + // > to generate procedural images by computing a color for + // > each pixel. ... + // + // The GLSL signature given is: + // + // > void mainImage( out vec4 fragColor, in vec2 fragCoord ); + // + // We can translate that signature almost verbatim into Slang: + // + void mainImage( out float4 fragColor, in float2 fragCoord ); + + // An image shader effect will thus be a Slang `struct` type + // that implements the `IShaderToyImageShader` interface. + // In most cases, effects will be created by pasting the + // GLSL content of an effect into boilerplate `struct` + // definition. + // + // As a result of scoping the GLSL effect code in a Slang + // `struct`, any functions declared in the effect will become + // methods of the struct, and any global-scope variables declared + // in the effect will become members of the `struct` type. + // + // Note: One caveat that arises here is that any effect that + // makes use of mutable global variables in its GLSL code will + // fail to compile with our approach. By default methods in + // Slang have can only read from `this` and its members, and + // need to be marked a `[mutating]` to have read-write access. + // Most shader toy effects only use globals to declare constants, + // so this limitation may not be a big problem in practice. + // + // One thing that *does* matter is that global variables/constants in + // an effect may have initializers, and the behavior of the + // effect is likely to depend on them being initialized correctly. + // + // In practice, the need to have effect-specific initialization + // is another requirement of the image shader interface that doesn't + // need to be explicitly explained on shadertoy.com, but does matter + // when writing an explicit interface in Slang. + // + // We express the requirement using a `static` method that + // returns the `This` type. Much like `this` refers to the + // "current object" with whatever type it might have at runtime, + // the `This` type refers to the "current type" that is implementing + // this interface. Static methods that return `This` allow + // Slang to express required "factory" functions in an interface. + // + static This getDefault(); +}; + +// Now that we have defined the interface that all image +// shader effects are expected to implement, we can define +// a vertex and fragment shader that can be used to evaluate +// any effect that conforms to the interface. +// +// The vertex shader is just going to implement a full-screen +// triangle, so it is almost trivial: +// +[shader("vertex")] +float4 vertexMain(float2 position : POSITION) + : SV_Position +{ + // TODO: We could even turn this into a shader that + // takes no inputs, and directly computes the XY + // location of each vertex based on `SV_VertexID` + + return float4(position, 0.5, 1.0); +} +// +// The body of the effect will run in the fragment shader, +// so that is where things get more interesting. +// +// We will define our fragment shader entry point as a +// Slang *generic function*, with a generic type parameter +// `T` that is constrained to implement the `IShaderToyImageShader` +// interface. +// +[shader("fragment")] +float4 fragmentMain( + float4 sv_position : SV_Position) + : SV_Target +{ + // Because the Slang compiler knows the interface that `T` + // is expected to implement, any code in the function + // body will be checked to make sure that it does not + // use operations that `T` would not be guaranteed to + // support. Unlike with traditional C++ templates or + // preprocessor-based shader specialization, it is possible + // to copmile and type-check this entry point once, + // and use it with multiple different types for `T`. + + // We start by creating an instance of the effect + // type `T`, initialized to whatever its default + // values are. + // + // Note: initializing the effect here preserves the + // semantics of the original shader toy code. An + // alternative approach (that would change the behavior + // from the original) would be to pass a value of + // type `T` in as a shader parameter of the entry point. + // + T toy = T.getDefault(); + + // Next, we invoke the user-defined effect by calling + // its `mainImage` function. + // + // Recall that the `fragColor` parameter to `mainImage` + // was defined as an `out` parameter, so this call + // will set a value into our local `fragColor` variable. + // + float2 fragCoord = sv_position.xy; + float4 fragColor = 0; + toy.mainImage(fragColor, fragCoord); + + // The output value from our shader is simply the result + // from the user-defined effect. + // + return float4(fragColor.xyz, 1); +} + +// By defining an interface for image shader effects, we +// have been able to decouple the code for the effects +// themselves from the code for their execution contexts. +// A key benefit of that decoupling is that we can introduce +// both new effects and new execution contexts in a modular +// fashion. +// +// For example, we can easily define a compute shader for +// executing an image shader effect: +// +[shader("compute")] +void computeMain( + uint3 sv_dispatchThreadID : SV_DispatchThreadID, + uniform RWTexture2D image) +{ + // The operations required to set up and execute + // the user-defined effect are similar to what + // they were for the fragment shader. + // + T toy = T.getDefault(); + + float2 fragCoord = float2(sv_dispatchThreadID.xy); + float4 fragColor = 0; + toy.mainImage(fragColor, fragCoord); + + // The main difference is that we now write the + // output color explicitly to an image pixel + // instead of relying on the rasterization pipeline. + // + image[sv_dispatchThreadID.xy] = fragColor; +} + +// At this point we've described how our module will +// execute shader toy effects that implement the +// required interface, but we also need to set up +// the services that those effects are able to use. +// +// The shader toy "How To" file describes a large number of uniform +// shader parameters that are implicitly visible to every effect. +// +// If we were able to design an interface from scratch, we might +// prefer to make the `mainImage` function take some kind of +// explicit context parameter that provides access to these +// values, but because our goal is to be compatible with existing +// effects with their established `mainImage` signature, we will +// instead define these parameters using old-fashioned global-scope +// shader parameters. +// +cbuffer ShaderToyUniforms +{ + // Note: We do not currently define all of the parameters + // exposed by Shader Toy, but rather just the most commonly + // used ones. + // + // TODO: We can and should fill in teh rest over time. + // + float4 iMouse; + float2 iResolution; + float iTime; +}; + +// In addition to the above parameters that use ordinary data types, +// shader toy also exposes the `iChannel*` parameters (`iChannel0` +// through `iChannel4`). These parameters represent sampled image +// inputs that can be bound to selected images as part of an effect. +// +// Traditional GLSL "sampler" types include both the texture image +// and sampler state, while Slang (like D3D, Vulkan, etc.) has +// distinct texture and sampler types. In order to define the +// channel variables in a way that is compatible with shader toy, +// we will define a `struct` type for a pair of a texture and +// a sampler: +// +struct TextureSamplerPair +{ + Texture2D t; + SamplerState s; +}; + +// With our texture-sampler pair type defined, we can introduce +// the variables for the texture channels easily. +// +TextureSamplerPair iChannel0; +TextureSamplerPair iChannel1; +TextureSamplerPair iChannel2; +TextureSamplerPair iChannel3; + + +// TODO: Shader toy supports more than just 2D textures, so a good +// avenue for extension of the module would be to define an interface +// for the texture channels, and have implementations using various +// forms of textures. +// +// A really ambitious idea would be include one example of the +// texture-channel interface that uses an existing ShaderToy as a +// procedural texture. + +// Shader toy effects access the contents of the `iChannel*` variables +// using the `texture()` function, so we need to provide a definition +// that is suitable: +// +float4 texture(TextureSamplerPair p, float3 uvw) +{ + // TODO: The right implementation to use here (at least + // in the context of fragment shaders) is: + // + // return p.t.Sample(p.s, uvw.xy); + // + // However, the current implementation of the main + // application code doesn't include texture image + // loading, so we will instead just fill in a + // placeholder result for "texture" lookup: + // + return 0.5; +} + +// The last major issue we need to address in this module is the way that +// shader toy effects are authored in GLSL, which has several differences +// from Slang that could cause problems. +// +// Some of these differences can be surmounted relatively easily. For +// example, GLSL uses different names for its built-in vector types, but +// for the most part they are compatible with those defined by HLSL/Slang. +// We can paper over this difference by defining a few helpful type +// aliases. +// +typealias vec2 = float2; +typealias vec3 = float3; +typealias vec4 = float4; + +// Matrix types in GLSL are a more subtle issue, because they have different +// semantics from their HLSL/Slang equivalents in a few key ways: +// +// * The infix `*` operator always performs component-wise multiplication +// in HLSL/Slang, but in GLSL it sometimes performs linear-algebraic +// products (whenever we have matrix*matrix, vector*matrix, or matrix*vector). +// HLSL/Slang require a distinct `mul()` function for those cases. +// +// * Because of differences in terminology and conventions, a linear-algebraic +// product like `M*v` in GLSL is equivalent to `mul(v, M)` in HLSL/Slang +// (note the reversed order of operands). +// +// * Constructing a matrix or vector from a single scalar consistently +// replicates that scalar across all components/elements in HLSL/Slang, +// but in GLSL it instead produces a diagonal matrix. +// +// These differences are not something we can surmount by defining the +// GLSL matrix types as aliases of the standard Slang ones, so instead we +// must define the GLSL matrix types as wrappers around the Slang ones. +// +struct mat2 +{ + float2x2 data; + + __init(float e00, float e01, float e10, float e11) + { + data = float2x2(e00, e01, e10, e11); + } + + // TODO: We need to fill in the other intializers and members + // available on matrices here. +}; + +// TODO: fill in `mat3` and `mat4`. + +// TODO: Ideally we would want to define overloaded operation functions +// to allow `*`, `*=`, etc. to apply to our user-space matrix types. +// +// Unfortunately, implementation bugs in the Slang compiler mean that +// user-defined operator overloads aren't working right now. +// +// vec2 operator*(vec2 left, mat2 right) +// { +// return mul(right.data, left); +// } +// +// void operator*=(inout vec2 left, mat2 right) +// { +// left = mul(right.data, left); +// } +// +// Instead, we will define an ordinary function for the one case that +// we 've run into in a test effect so far: +// +void mulAssign(inout vec2 left, mat2 right) +{ + left = mul(right.data, left); +} + +float fract(float value) +{ + return frac(value); +} + +float mix(float a, float b, float t) +{ + return lerp(a, b, t); +} \ No newline at end of file -- cgit v1.2.3