From d155eaa92d56a4ec00109d25c8c70fe12fb96c2e Mon Sep 17 00:00:00 2001 From: jsmall-nvidia Date: Mon, 7 Jan 2019 09:14:01 -0500 Subject: Feature/unique tool source names (#766) * Remove AppContext. Use StdChannels to hold writers, and TestToolUtil to hold test tool specific functionality. * StdChannels -> StdWriters * getStdOut -> getOut, getStdError -> getError * Renamed main.cpp files of tools to try and stop visual studio getting confused between files - such that clicking on an error takes editor to the right location. --- tools/render-test/main.cpp | 679 ------- tools/render-test/render-test-main.cpp | 679 +++++++ tools/render-test/render-test-tool.vcxproj | 2 +- tools/render-test/render-test-tool.vcxproj.filters | 6 +- tools/slang-reflection-test/main.cpp | 935 ---------- .../slang-reflection-test-main.cpp | 935 ++++++++++ .../slang-reflection-test-tool.vcxproj | 2 +- .../slang-reflection-test-tool.vcxproj.filters | 2 +- tools/slang-test/main.cpp | 1905 -------------------- tools/slang-test/slang-test-main.cpp | 1905 ++++++++++++++++++++ tools/slang-test/slang-test.vcxproj | 2 +- tools/slang-test/slang-test.vcxproj.filters | 6 +- 12 files changed, 3529 insertions(+), 3529 deletions(-) delete mode 100644 tools/render-test/main.cpp create mode 100644 tools/render-test/render-test-main.cpp delete mode 100644 tools/slang-reflection-test/main.cpp create mode 100644 tools/slang-reflection-test/slang-reflection-test-main.cpp delete mode 100644 tools/slang-test/main.cpp create mode 100644 tools/slang-test/slang-test-main.cpp (limited to 'tools') diff --git a/tools/render-test/main.cpp b/tools/render-test/main.cpp deleted file mode 100644 index 69874ac04..000000000 --- a/tools/render-test/main.cpp +++ /dev/null @@ -1,679 +0,0 @@ -// main.cpp - -#include "options.h" -#include "render.h" -#include "render-d3d11.h" -#include "render-d3d12.h" -#include "render-gl.h" -#include "render-vk.h" - -#include "slang-support.h" -#include "surface.h" -#include "png-serialize-util.h" - -#include "shader-renderer-util.h" - -#include "shader-input-layout.h" -#include -#include - -#include "../../source/core/slang-test-tool-util.h" - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#undef WIN32_LEAN_AND_MEAN -#undef NOMINMAX - -#ifdef _MSC_VER -#pragma warning(disable: 4996) -#endif - -namespace renderer_test { - -using Slang::Result; - -int gWindowWidth = 1024; -int gWindowHeight = 768; - -class Window: public RefObject -{ -public: - SlangResult initialize(int width, int height); - - void show(); - - void* getHandle() const { return m_hwnd; } - - Window() {} - ~Window(); - - static LRESULT CALLBACK windowProc(HWND windowHandle, - UINT message, - WPARAM wParam, - LPARAM lParam); - -protected: - - HINSTANCE m_hinst = nullptr; - HWND m_hwnd = nullptr; -}; - -// -// We use a bare-minimum window procedure to get things up and running. -// - -/* static */LRESULT CALLBACK Window::windowProc( - HWND windowHandle, - UINT message, - WPARAM wParam, - LPARAM lParam) -{ - switch (message) - { - case WM_CLOSE: - PostQuitMessage(0); - return 0; - } - - return DefWindowProcW(windowHandle, message, wParam, lParam); -} - -static ATOM _getWindowClassAtom(HINSTANCE hinst) -{ - static ATOM s_windowClassAtom; - - if (s_windowClassAtom) - { - return s_windowClassAtom; - } - WNDCLASSEXW windowClassDesc; - windowClassDesc.cbSize = sizeof(windowClassDesc); - windowClassDesc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; - windowClassDesc.lpfnWndProc = &Window::windowProc; - windowClassDesc.cbClsExtra = 0; - windowClassDesc.cbWndExtra = 0; - windowClassDesc.hInstance = hinst; - windowClassDesc.hIcon = 0; - windowClassDesc.hCursor = 0; - windowClassDesc.hbrBackground = 0; - windowClassDesc.lpszMenuName = 0; - windowClassDesc.lpszClassName = L"SlangRenderTest"; - windowClassDesc.hIconSm = 0; - s_windowClassAtom = RegisterClassExW(&windowClassDesc); - - return s_windowClassAtom; -} - -SlangResult Window::initialize(int widthIn, int heightIn) -{ - // Do initial window-creation stuff here, rather than in the renderer-specific files - - m_hinst = GetModuleHandleA(0); - - // First we register a window class. - ATOM windowClassAtom = _getWindowClassAtom(m_hinst); - if (!windowClassAtom) - { - fprintf(stderr, "error: failed to register window class\n"); - return SLANG_FAIL; - } - - // Next, we create a window using that window class. - - // We will create a borderless window since our screen-capture logic in GL - // seems to get thrown off by having to deal with a window frame. - DWORD windowStyle = WS_POPUP; - DWORD windowExtendedStyle = 0; - - RECT windowRect = { 0, 0, widthIn, heightIn }; - AdjustWindowRectEx(&windowRect, windowStyle, /*hasMenu=*/false, windowExtendedStyle); - - { - auto width = windowRect.right - windowRect.left; - auto height = windowRect.bottom - windowRect.top; - - LPWSTR windowName = L"Slang Render Test"; - m_hwnd = CreateWindowExW( - windowExtendedStyle, - (LPWSTR)windowClassAtom, - windowName, - windowStyle, - 0, 0, // x, y - width, height, - NULL, // parent - NULL, // menu - m_hinst, - NULL); - } - if (!m_hwnd) - { - fprintf(stderr, "error: failed to create window\n"); - return SLANG_FAIL; - } - - return SLANG_OK; -} - - -void Window::show() -{ - // Once initialization is all complete, we show the window... - int showCommand = SW_SHOW; - ShowWindow(m_hwnd, showCommand); -} - -Window::~Window() -{ - if (m_hwnd) - { - DestroyWindow(m_hwnd); - } -} - -// -// For the purposes of a small example, we will define the vertex data for a -// single triangle directly in the source file. It should be easy to extend -// this example to load data from an external source, if desired. -// - -struct Vertex -{ - float position[3]; - float color[3]; - float uv[2]; -}; - -static const Vertex kVertexData[] = -{ - { { 0, 0, 0.5 }, {1, 0, 0} , {0, 0} }, - { { 0, 1, 0.5 }, {0, 0, 1} , {1, 0} }, - { { 1, 0, 0.5 }, {0, 1, 0} , {1, 1} }, -}; -static const int kVertexCount = SLANG_COUNT_OF(kVertexData); - -using namespace Slang; - -class RenderTestApp -{ - public: - - // At initialization time, we are going to load and compile our Slang shader - // code, and then create the API objects we need for rendering. - Result initialize(Renderer* renderer, ShaderCompiler* shaderCompiler); - void runCompute(); - void renderFrame(); - void finalize(); - - BindingStateImpl* getBindingState() const { return m_bindingState; } - - Result writeBindingOutput(const char* fileName); - - Result writeScreen(const char* filename); - - protected: - /// Called in initialize - Result initializeShaders(ShaderCompiler* shaderCompiler); - - // variables for state to be used for rendering... - uintptr_t m_constantBufferSize, m_computeResultBufferSize; - - RefPtr m_renderer; - - RefPtr m_constantBuffer; - RefPtr m_inputLayout; - RefPtr m_vertexBuffer; - RefPtr m_shaderProgram; - RefPtr m_pipelineState; - RefPtr m_bindingState; - - ShaderInputLayout m_shaderInputLayout; ///< The binding layout - int m_numAddedConstantBuffers; ///< Constant buffers can be added to the binding directly. Will be added at the end. -}; - -// Entry point name to use for vertex/fragment shader -static const char vertexEntryPointName[] = "vertexMain"; -static const char fragmentEntryPointName[] = "fragmentMain"; -static const char computeEntryPointName[] = "computeMain"; - -SlangResult RenderTestApp::initialize(Renderer* renderer, ShaderCompiler* shaderCompiler) -{ - SLANG_RETURN_ON_FAIL(initializeShaders(shaderCompiler)); - - m_numAddedConstantBuffers = 0; - m_renderer = renderer; - - // TODO(tfoley): use each API's reflection interface to query the constant-buffer size needed - { - m_constantBufferSize = 16 * sizeof(float); - - BufferResource::Desc constantBufferDesc; - constantBufferDesc.init(m_constantBufferSize); - constantBufferDesc.cpuAccessFlags = Resource::AccessFlag::Write; - - m_constantBuffer = renderer->createBufferResource(Resource::Usage::ConstantBuffer, constantBufferDesc); - if (!m_constantBuffer) - return SLANG_FAIL; - } - - { - //! Hack -> if doing a graphics test, add an extra binding for our dynamic constant buffer - // - // TODO: Should probably be more sophisticated than this - with 'dynamic' constant buffer/s binding always being specified - // in the test file - RefPtr addedConstantBuffer; - switch(gOptions.shaderType) - { - default: - break; - - case Options::ShaderProgramType::Graphics: - case Options::ShaderProgramType::GraphicsCompute: - addedConstantBuffer = m_constantBuffer; - m_numAddedConstantBuffers++; - break; - } - - BindingStateImpl* bindingState = nullptr; - SLANG_RETURN_ON_FAIL(ShaderRendererUtil::createBindingState(m_shaderInputLayout, m_renderer, addedConstantBuffer, &bindingState)); - m_bindingState = bindingState; - } - - // Do other initialization that doesn't depend on the source language. - - // Input Assembler (IA) - - const InputElementDesc inputElements[] = { - { "A", 0, Format::RGB_Float32, offsetof(Vertex, position) }, - { "A", 1, Format::RGB_Float32, offsetof(Vertex, color) }, - { "A", 2, Format::RG_Float32, offsetof(Vertex, uv) }, - }; - - m_inputLayout = renderer->createInputLayout(inputElements, SLANG_COUNT_OF(inputElements)); - if(!m_inputLayout) - return SLANG_FAIL; - - BufferResource::Desc vertexBufferDesc; - vertexBufferDesc.init(kVertexCount * sizeof(Vertex)); - - m_vertexBuffer = renderer->createBufferResource(Resource::Usage::VertexBuffer, vertexBufferDesc, kVertexData); - if(!m_vertexBuffer) - return SLANG_FAIL; - - { - switch(gOptions.shaderType) - { - default: - assert(!"unexpected test shader type"); - return SLANG_FAIL; - - case Options::ShaderProgramType::Compute: - { - ComputePipelineStateDesc desc; - desc.pipelineLayout = m_bindingState->pipelineLayout; - desc.program = m_shaderProgram; - - m_pipelineState = renderer->createComputePipelineState(desc); - } - break; - - case Options::ShaderProgramType::Graphics: - case Options::ShaderProgramType::GraphicsCompute: - { - GraphicsPipelineStateDesc desc; - desc.pipelineLayout = m_bindingState->pipelineLayout; - desc.program = m_shaderProgram; - desc.inputLayout = m_inputLayout; - desc.renderTargetCount = m_bindingState->m_numRenderTargets; - - m_pipelineState = renderer->createGraphicsPipelineState(desc); - } - break; - } - } - - return SLANG_OK; -} - -Result RenderTestApp::initializeShaders(ShaderCompiler* shaderCompiler) -{ - // Read in the source code - char const* sourcePath = gOptions.sourcePath; - FILE* sourceFile = fopen(sourcePath, "rb"); - if (!sourceFile) - { - fprintf(stderr, "error: failed to open '%s' for reading\n", sourcePath); - return SLANG_FAIL; - } - fseek(sourceFile, 0, SEEK_END); - size_t sourceSize = ftell(sourceFile); - fseek(sourceFile, 0, SEEK_SET); - - List sourceText; - sourceText.SetSize(sourceSize + 1); - fread(sourceText.Buffer(), sourceSize, 1, sourceFile); - fclose(sourceFile); - sourceText[sourceSize] = 0; - - switch( gOptions.shaderType ) - { - default: - m_shaderInputLayout.numRenderTargets = 1; - break; - - case Options::ShaderProgramType::Compute: - m_shaderInputLayout.numRenderTargets = 0; - break; - } - m_shaderInputLayout.Parse(sourceText.Buffer()); - - ShaderCompileRequest::SourceInfo sourceInfo; - sourceInfo.path = sourcePath; - sourceInfo.dataBegin = sourceText.Buffer(); - sourceInfo.dataEnd = sourceText.Buffer() + sourceSize; - - ShaderCompileRequest compileRequest; - compileRequest.source = sourceInfo; - if (gOptions.shaderType == Options::ShaderProgramType::Graphics || gOptions.shaderType == Options::ShaderProgramType::GraphicsCompute) - { - compileRequest.vertexShader.source = sourceInfo; - compileRequest.vertexShader.name = vertexEntryPointName; - compileRequest.fragmentShader.source = sourceInfo; - compileRequest.fragmentShader.name = fragmentEntryPointName; - } - else - { - compileRequest.computeShader.source = sourceInfo; - compileRequest.computeShader.name = computeEntryPointName; - } - compileRequest.entryPointTypeArguments = m_shaderInputLayout.globalTypeArguments; - m_shaderProgram = shaderCompiler->compileProgram(compileRequest); - if (!m_shaderProgram) - { - return SLANG_FAIL; - } - - return SLANG_OK; -} - -void RenderTestApp::renderFrame() -{ - auto mappedData = m_renderer->map(m_constantBuffer, MapFlavor::WriteDiscard); - if(mappedData) - { - const ProjectionStyle projectionStyle = RendererUtil::getProjectionStyle(m_renderer->getRendererType()); - RendererUtil::getIdentityProjection(projectionStyle, (float*)mappedData); - - m_renderer->unmap(m_constantBuffer); - } - - auto pipelineType = PipelineType::Graphics; - - m_renderer->setPipelineState(pipelineType, m_pipelineState); - - m_renderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); - m_renderer->setVertexBuffer(0, m_vertexBuffer, sizeof(Vertex)); - - m_bindingState->apply(m_renderer, pipelineType); - - m_renderer->draw(3); -} - -void RenderTestApp::runCompute() -{ - auto pipelineType = PipelineType::Compute; - m_renderer->setPipelineState(pipelineType, m_pipelineState); - m_bindingState->apply(m_renderer, pipelineType); - m_renderer->dispatchCompute(1, 1, 1); -} - -void RenderTestApp::finalize() -{ -} - -Result RenderTestApp::writeBindingOutput(const char* fileName) -{ - // Submit the work - m_renderer->submitGpuWork(); - // Wait until everything is complete - m_renderer->waitForGpu(); - - FILE * f = fopen(fileName, "wb"); - if (!f) - { - return SLANG_FAIL; - } - - for(auto binding : m_bindingState->outputBindings) - { - auto i = binding.entryIndex; - const auto& layoutBinding = m_shaderInputLayout.entries[i]; - - assert(layoutBinding.isOutput); - { - if (binding.resource && binding.resource->isBuffer()) - { - BufferResource* bufferResource = static_cast(binding.resource.Ptr()); - const size_t bufferSize = bufferResource->getDesc().sizeInBytes; - - unsigned int* ptr = (unsigned int*)m_renderer->map(bufferResource, MapFlavor::HostRead); - if (!ptr) - { - fclose(f); - return SLANG_FAIL; - } - - const int size = int(bufferSize / sizeof(unsigned int)); - for (int i = 0; i < size; ++i) - { - fprintf(f, "%X\n", ptr[i]); - } - m_renderer->unmap(bufferResource); - } - else - { - printf("invalid output type at %d.\n", int(i)); - } - } - } - fclose(f); - - return SLANG_OK; -} - - -Result RenderTestApp::writeScreen(const char* filename) -{ - Surface surface; - SLANG_RETURN_ON_FAIL(m_renderer->captureScreenSurface(surface)); - return PngSerializeUtil::write(filename, surface); -} - -} // namespace renderer_test - -SLANG_TEST_TOOL_API SlangResult innerMain(Slang::StdWriters* stdWriters, SlangSession* session, int argcIn, const char*const* argvIn) -{ - using namespace renderer_test; - using namespace Slang; - - StdWriters::setSingleton(stdWriters); - - // Parse command-line options - SLANG_RETURN_ON_FAIL(parseOptions(argcIn, argvIn, StdWriters::getError())); - - RefPtr window(new renderer_test::Window); - SLANG_RETURN_ON_FAIL(window->initialize(gWindowWidth, gWindowHeight)); - - Slang::RefPtr renderer; - - SlangSourceLanguage nativeLanguage = SLANG_SOURCE_LANGUAGE_UNKNOWN; - SlangCompileTarget slangTarget = SLANG_TARGET_NONE; - SlangPassThrough slangPassThrough = SLANG_PASS_THROUGH_NONE; - char const* profileName = ""; - switch (gOptions.rendererType) - { - case RendererType::DirectX11: - renderer = createD3D11Renderer(); - slangTarget = SLANG_DXBC; - nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL; - slangPassThrough = SLANG_PASS_THROUGH_FXC; - profileName = "sm_5_0"; - break; - - case RendererType::DirectX12: - renderer = createD3D12Renderer(); - slangTarget = SLANG_DXBC; - nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL; - slangPassThrough = SLANG_PASS_THROUGH_FXC; - profileName = "sm_5_0"; - if( gOptions.useDXIL ) - { - slangTarget = SLANG_DXIL; - slangPassThrough = SLANG_PASS_THROUGH_DXC; - profileName = "sm_6_0"; - } - break; - - case RendererType::OpenGl: - renderer = createGLRenderer(); - slangTarget = SLANG_GLSL; - nativeLanguage = SLANG_SOURCE_LANGUAGE_GLSL; - slangPassThrough = SLANG_PASS_THROUGH_GLSLANG; - profileName = "glsl_430"; - break; - - case RendererType::Vulkan: - renderer = createVKRenderer(); - slangTarget = SLANG_SPIRV; - nativeLanguage = SLANG_SOURCE_LANGUAGE_GLSL; - slangPassThrough = SLANG_PASS_THROUGH_GLSLANG; - profileName = "glsl_430"; - break; - - default: - fprintf(stderr, "error: unexpected\n"); - return SLANG_FAIL; - } - - if (!renderer) - { - fprintf(stderr, "Unable to create renderer\n"); - return SLANG_FAIL; - } - - Renderer::Desc desc; - desc.width = gWindowWidth; - desc.height = gWindowHeight; - - { - SlangResult res = renderer->initialize(desc, (HWND)window->getHandle()); - if (SLANG_FAILED(res)) - { - fprintf(stderr, "Unable to initialize renderer\n"); - return res; - } - } - - ShaderCompiler shaderCompiler; - shaderCompiler.renderer = renderer; - shaderCompiler.target = slangTarget; - shaderCompiler.profile = profileName; - shaderCompiler.slangSession = session; - - switch (gOptions.inputLanguageID) - { - case Options::InputLanguageID::Slang: - shaderCompiler.sourceLanguage = SLANG_SOURCE_LANGUAGE_SLANG; - shaderCompiler.passThrough = SLANG_PASS_THROUGH_NONE; - break; - - case Options::InputLanguageID::Native: - shaderCompiler.sourceLanguage = nativeLanguage; - shaderCompiler.passThrough = slangPassThrough; - break; - - default: - break; - } - - { - RenderTestApp app; - - SLANG_RETURN_ON_FAIL(app.initialize(renderer, &shaderCompiler)); - - window->show(); - - // ... and enter the event loop: - for (;;) - { - MSG message; - - int result = PeekMessageW(&message, NULL, 0, 0, PM_REMOVE); - if (result != 0) - { - if (message.message == WM_QUIT) - { - return (int)message.wParam; - } - - TranslateMessage(&message); - DispatchMessageW(&message); - } - else - { - // Whenever we don't have Windows events to process, we render a frame. - if (gOptions.shaderType == Options::ShaderProgramType::Compute) - { - app.runCompute(); - } - else - { - static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 }; - renderer->setClearColor(kClearColor); - renderer->clearFrame(); - - app.renderFrame(); - } - // If we are in a mode where output is requested, we need to snapshot the back buffer here - if (gOptions.outputPath) - { - // Submit the work - renderer->submitGpuWork(); - // Wait until everything is complete - renderer->waitForGpu(); - - if (gOptions.shaderType == Options::ShaderProgramType::Compute || gOptions.shaderType == Options::ShaderProgramType::GraphicsCompute) - { - SLANG_RETURN_ON_FAIL(app.writeBindingOutput(gOptions.outputPath)); - } - else - { - SlangResult res = app.writeScreen(gOptions.outputPath); - - if (SLANG_FAILED(res)) - { - fprintf(stderr, "ERROR: failed to write screen capture to file\n"); - return res; - } - } - return SLANG_OK; - } - - renderer->presentFrame(); - } - } - } - - return SLANG_OK; -} - - -int main(int argc, char** argv) -{ - SlangSession* session = spCreateSession(nullptr); - SlangResult res = innerMain(Slang::StdWriters::initDefault(), session, argc, argv); - spDestroySession(session); - - return SLANG_FAILED(res) ? 1 : 0; -} - diff --git a/tools/render-test/render-test-main.cpp b/tools/render-test/render-test-main.cpp new file mode 100644 index 000000000..7cf303631 --- /dev/null +++ b/tools/render-test/render-test-main.cpp @@ -0,0 +1,679 @@ +// render-test-main.cpp + +#include "options.h" +#include "render.h" +#include "render-d3d11.h" +#include "render-d3d12.h" +#include "render-gl.h" +#include "render-vk.h" + +#include "slang-support.h" +#include "surface.h" +#include "png-serialize-util.h" + +#include "shader-renderer-util.h" + +#include "shader-input-layout.h" +#include +#include + +#include "../../source/core/slang-test-tool-util.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#undef WIN32_LEAN_AND_MEAN +#undef NOMINMAX + +#ifdef _MSC_VER +#pragma warning(disable: 4996) +#endif + +namespace renderer_test { + +using Slang::Result; + +int gWindowWidth = 1024; +int gWindowHeight = 768; + +class Window: public RefObject +{ +public: + SlangResult initialize(int width, int height); + + void show(); + + void* getHandle() const { return m_hwnd; } + + Window() {} + ~Window(); + + static LRESULT CALLBACK windowProc(HWND windowHandle, + UINT message, + WPARAM wParam, + LPARAM lParam); + +protected: + + HINSTANCE m_hinst = nullptr; + HWND m_hwnd = nullptr; +}; + +// +// We use a bare-minimum window procedure to get things up and running. +// + +/* static */LRESULT CALLBACK Window::windowProc( + HWND windowHandle, + UINT message, + WPARAM wParam, + LPARAM lParam) +{ + switch (message) + { + case WM_CLOSE: + PostQuitMessage(0); + return 0; + } + + return DefWindowProcW(windowHandle, message, wParam, lParam); +} + +static ATOM _getWindowClassAtom(HINSTANCE hinst) +{ + static ATOM s_windowClassAtom; + + if (s_windowClassAtom) + { + return s_windowClassAtom; + } + WNDCLASSEXW windowClassDesc; + windowClassDesc.cbSize = sizeof(windowClassDesc); + windowClassDesc.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; + windowClassDesc.lpfnWndProc = &Window::windowProc; + windowClassDesc.cbClsExtra = 0; + windowClassDesc.cbWndExtra = 0; + windowClassDesc.hInstance = hinst; + windowClassDesc.hIcon = 0; + windowClassDesc.hCursor = 0; + windowClassDesc.hbrBackground = 0; + windowClassDesc.lpszMenuName = 0; + windowClassDesc.lpszClassName = L"SlangRenderTest"; + windowClassDesc.hIconSm = 0; + s_windowClassAtom = RegisterClassExW(&windowClassDesc); + + return s_windowClassAtom; +} + +SlangResult Window::initialize(int widthIn, int heightIn) +{ + // Do initial window-creation stuff here, rather than in the renderer-specific files + + m_hinst = GetModuleHandleA(0); + + // First we register a window class. + ATOM windowClassAtom = _getWindowClassAtom(m_hinst); + if (!windowClassAtom) + { + fprintf(stderr, "error: failed to register window class\n"); + return SLANG_FAIL; + } + + // Next, we create a window using that window class. + + // We will create a borderless window since our screen-capture logic in GL + // seems to get thrown off by having to deal with a window frame. + DWORD windowStyle = WS_POPUP; + DWORD windowExtendedStyle = 0; + + RECT windowRect = { 0, 0, widthIn, heightIn }; + AdjustWindowRectEx(&windowRect, windowStyle, /*hasMenu=*/false, windowExtendedStyle); + + { + auto width = windowRect.right - windowRect.left; + auto height = windowRect.bottom - windowRect.top; + + LPWSTR windowName = L"Slang Render Test"; + m_hwnd = CreateWindowExW( + windowExtendedStyle, + (LPWSTR)windowClassAtom, + windowName, + windowStyle, + 0, 0, // x, y + width, height, + NULL, // parent + NULL, // menu + m_hinst, + NULL); + } + if (!m_hwnd) + { + fprintf(stderr, "error: failed to create window\n"); + return SLANG_FAIL; + } + + return SLANG_OK; +} + + +void Window::show() +{ + // Once initialization is all complete, we show the window... + int showCommand = SW_SHOW; + ShowWindow(m_hwnd, showCommand); +} + +Window::~Window() +{ + if (m_hwnd) + { + DestroyWindow(m_hwnd); + } +} + +// +// For the purposes of a small example, we will define the vertex data for a +// single triangle directly in the source file. It should be easy to extend +// this example to load data from an external source, if desired. +// + +struct Vertex +{ + float position[3]; + float color[3]; + float uv[2]; +}; + +static const Vertex kVertexData[] = +{ + { { 0, 0, 0.5 }, {1, 0, 0} , {0, 0} }, + { { 0, 1, 0.5 }, {0, 0, 1} , {1, 0} }, + { { 1, 0, 0.5 }, {0, 1, 0} , {1, 1} }, +}; +static const int kVertexCount = SLANG_COUNT_OF(kVertexData); + +using namespace Slang; + +class RenderTestApp +{ + public: + + // At initialization time, we are going to load and compile our Slang shader + // code, and then create the API objects we need for rendering. + Result initialize(Renderer* renderer, ShaderCompiler* shaderCompiler); + void runCompute(); + void renderFrame(); + void finalize(); + + BindingStateImpl* getBindingState() const { return m_bindingState; } + + Result writeBindingOutput(const char* fileName); + + Result writeScreen(const char* filename); + + protected: + /// Called in initialize + Result initializeShaders(ShaderCompiler* shaderCompiler); + + // variables for state to be used for rendering... + uintptr_t m_constantBufferSize, m_computeResultBufferSize; + + RefPtr m_renderer; + + RefPtr m_constantBuffer; + RefPtr m_inputLayout; + RefPtr m_vertexBuffer; + RefPtr m_shaderProgram; + RefPtr m_pipelineState; + RefPtr m_bindingState; + + ShaderInputLayout m_shaderInputLayout; ///< The binding layout + int m_numAddedConstantBuffers; ///< Constant buffers can be added to the binding directly. Will be added at the end. +}; + +// Entry point name to use for vertex/fragment shader +static const char vertexEntryPointName[] = "vertexMain"; +static const char fragmentEntryPointName[] = "fragmentMain"; +static const char computeEntryPointName[] = "computeMain"; + +SlangResult RenderTestApp::initialize(Renderer* renderer, ShaderCompiler* shaderCompiler) +{ + SLANG_RETURN_ON_FAIL(initializeShaders(shaderCompiler)); + + m_numAddedConstantBuffers = 0; + m_renderer = renderer; + + // TODO(tfoley): use each API's reflection interface to query the constant-buffer size needed + { + m_constantBufferSize = 16 * sizeof(float); + + BufferResource::Desc constantBufferDesc; + constantBufferDesc.init(m_constantBufferSize); + constantBufferDesc.cpuAccessFlags = Resource::AccessFlag::Write; + + m_constantBuffer = renderer->createBufferResource(Resource::Usage::ConstantBuffer, constantBufferDesc); + if (!m_constantBuffer) + return SLANG_FAIL; + } + + { + //! Hack -> if doing a graphics test, add an extra binding for our dynamic constant buffer + // + // TODO: Should probably be more sophisticated than this - with 'dynamic' constant buffer/s binding always being specified + // in the test file + RefPtr addedConstantBuffer; + switch(gOptions.shaderType) + { + default: + break; + + case Options::ShaderProgramType::Graphics: + case Options::ShaderProgramType::GraphicsCompute: + addedConstantBuffer = m_constantBuffer; + m_numAddedConstantBuffers++; + break; + } + + BindingStateImpl* bindingState = nullptr; + SLANG_RETURN_ON_FAIL(ShaderRendererUtil::createBindingState(m_shaderInputLayout, m_renderer, addedConstantBuffer, &bindingState)); + m_bindingState = bindingState; + } + + // Do other initialization that doesn't depend on the source language. + + // Input Assembler (IA) + + const InputElementDesc inputElements[] = { + { "A", 0, Format::RGB_Float32, offsetof(Vertex, position) }, + { "A", 1, Format::RGB_Float32, offsetof(Vertex, color) }, + { "A", 2, Format::RG_Float32, offsetof(Vertex, uv) }, + }; + + m_inputLayout = renderer->createInputLayout(inputElements, SLANG_COUNT_OF(inputElements)); + if(!m_inputLayout) + return SLANG_FAIL; + + BufferResource::Desc vertexBufferDesc; + vertexBufferDesc.init(kVertexCount * sizeof(Vertex)); + + m_vertexBuffer = renderer->createBufferResource(Resource::Usage::VertexBuffer, vertexBufferDesc, kVertexData); + if(!m_vertexBuffer) + return SLANG_FAIL; + + { + switch(gOptions.shaderType) + { + default: + assert(!"unexpected test shader type"); + return SLANG_FAIL; + + case Options::ShaderProgramType::Compute: + { + ComputePipelineStateDesc desc; + desc.pipelineLayout = m_bindingState->pipelineLayout; + desc.program = m_shaderProgram; + + m_pipelineState = renderer->createComputePipelineState(desc); + } + break; + + case Options::ShaderProgramType::Graphics: + case Options::ShaderProgramType::GraphicsCompute: + { + GraphicsPipelineStateDesc desc; + desc.pipelineLayout = m_bindingState->pipelineLayout; + desc.program = m_shaderProgram; + desc.inputLayout = m_inputLayout; + desc.renderTargetCount = m_bindingState->m_numRenderTargets; + + m_pipelineState = renderer->createGraphicsPipelineState(desc); + } + break; + } + } + + return SLANG_OK; +} + +Result RenderTestApp::initializeShaders(ShaderCompiler* shaderCompiler) +{ + // Read in the source code + char const* sourcePath = gOptions.sourcePath; + FILE* sourceFile = fopen(sourcePath, "rb"); + if (!sourceFile) + { + fprintf(stderr, "error: failed to open '%s' for reading\n", sourcePath); + return SLANG_FAIL; + } + fseek(sourceFile, 0, SEEK_END); + size_t sourceSize = ftell(sourceFile); + fseek(sourceFile, 0, SEEK_SET); + + List sourceText; + sourceText.SetSize(sourceSize + 1); + fread(sourceText.Buffer(), sourceSize, 1, sourceFile); + fclose(sourceFile); + sourceText[sourceSize] = 0; + + switch( gOptions.shaderType ) + { + default: + m_shaderInputLayout.numRenderTargets = 1; + break; + + case Options::ShaderProgramType::Compute: + m_shaderInputLayout.numRenderTargets = 0; + break; + } + m_shaderInputLayout.Parse(sourceText.Buffer()); + + ShaderCompileRequest::SourceInfo sourceInfo; + sourceInfo.path = sourcePath; + sourceInfo.dataBegin = sourceText.Buffer(); + sourceInfo.dataEnd = sourceText.Buffer() + sourceSize; + + ShaderCompileRequest compileRequest; + compileRequest.source = sourceInfo; + if (gOptions.shaderType == Options::ShaderProgramType::Graphics || gOptions.shaderType == Options::ShaderProgramType::GraphicsCompute) + { + compileRequest.vertexShader.source = sourceInfo; + compileRequest.vertexShader.name = vertexEntryPointName; + compileRequest.fragmentShader.source = sourceInfo; + compileRequest.fragmentShader.name = fragmentEntryPointName; + } + else + { + compileRequest.computeShader.source = sourceInfo; + compileRequest.computeShader.name = computeEntryPointName; + } + compileRequest.entryPointTypeArguments = m_shaderInputLayout.globalTypeArguments; + m_shaderProgram = shaderCompiler->compileProgram(compileRequest); + if (!m_shaderProgram) + { + return SLANG_FAIL; + } + + return SLANG_OK; +} + +void RenderTestApp::renderFrame() +{ + auto mappedData = m_renderer->map(m_constantBuffer, MapFlavor::WriteDiscard); + if(mappedData) + { + const ProjectionStyle projectionStyle = RendererUtil::getProjectionStyle(m_renderer->getRendererType()); + RendererUtil::getIdentityProjection(projectionStyle, (float*)mappedData); + + m_renderer->unmap(m_constantBuffer); + } + + auto pipelineType = PipelineType::Graphics; + + m_renderer->setPipelineState(pipelineType, m_pipelineState); + + m_renderer->setPrimitiveTopology(PrimitiveTopology::TriangleList); + m_renderer->setVertexBuffer(0, m_vertexBuffer, sizeof(Vertex)); + + m_bindingState->apply(m_renderer, pipelineType); + + m_renderer->draw(3); +} + +void RenderTestApp::runCompute() +{ + auto pipelineType = PipelineType::Compute; + m_renderer->setPipelineState(pipelineType, m_pipelineState); + m_bindingState->apply(m_renderer, pipelineType); + m_renderer->dispatchCompute(1, 1, 1); +} + +void RenderTestApp::finalize() +{ +} + +Result RenderTestApp::writeBindingOutput(const char* fileName) +{ + // Submit the work + m_renderer->submitGpuWork(); + // Wait until everything is complete + m_renderer->waitForGpu(); + + FILE * f = fopen(fileName, "wb"); + if (!f) + { + return SLANG_FAIL; + } + + for(auto binding : m_bindingState->outputBindings) + { + auto i = binding.entryIndex; + const auto& layoutBinding = m_shaderInputLayout.entries[i]; + + assert(layoutBinding.isOutput); + { + if (binding.resource && binding.resource->isBuffer()) + { + BufferResource* bufferResource = static_cast(binding.resource.Ptr()); + const size_t bufferSize = bufferResource->getDesc().sizeInBytes; + + unsigned int* ptr = (unsigned int*)m_renderer->map(bufferResource, MapFlavor::HostRead); + if (!ptr) + { + fclose(f); + return SLANG_FAIL; + } + + const int size = int(bufferSize / sizeof(unsigned int)); + for (int i = 0; i < size; ++i) + { + fprintf(f, "%X\n", ptr[i]); + } + m_renderer->unmap(bufferResource); + } + else + { + printf("invalid output type at %d.\n", int(i)); + } + } + } + fclose(f); + + return SLANG_OK; +} + + +Result RenderTestApp::writeScreen(const char* filename) +{ + Surface surface; + SLANG_RETURN_ON_FAIL(m_renderer->captureScreenSurface(surface)); + return PngSerializeUtil::write(filename, surface); +} + +} // namespace renderer_test + +SLANG_TEST_TOOL_API SlangResult innerMain(Slang::StdWriters* stdWriters, SlangSession* session, int argcIn, const char*const* argvIn) +{ + using namespace renderer_test; + using namespace Slang; + + StdWriters::setSingleton(stdWriters); + + // Parse command-line options + SLANG_RETURN_ON_FAIL(parseOptions(argcIn, argvIn, StdWriters::getError())); + + RefPtr window(new renderer_test::Window); + SLANG_RETURN_ON_FAIL(window->initialize(gWindowWidth, gWindowHeight)); + + Slang::RefPtr renderer; + + SlangSourceLanguage nativeLanguage = SLANG_SOURCE_LANGUAGE_UNKNOWN; + SlangCompileTarget slangTarget = SLANG_TARGET_NONE; + SlangPassThrough slangPassThrough = SLANG_PASS_THROUGH_NONE; + char const* profileName = ""; + switch (gOptions.rendererType) + { + case RendererType::DirectX11: + renderer = createD3D11Renderer(); + slangTarget = SLANG_DXBC; + nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL; + slangPassThrough = SLANG_PASS_THROUGH_FXC; + profileName = "sm_5_0"; + break; + + case RendererType::DirectX12: + renderer = createD3D12Renderer(); + slangTarget = SLANG_DXBC; + nativeLanguage = SLANG_SOURCE_LANGUAGE_HLSL; + slangPassThrough = SLANG_PASS_THROUGH_FXC; + profileName = "sm_5_0"; + if( gOptions.useDXIL ) + { + slangTarget = SLANG_DXIL; + slangPassThrough = SLANG_PASS_THROUGH_DXC; + profileName = "sm_6_0"; + } + break; + + case RendererType::OpenGl: + renderer = createGLRenderer(); + slangTarget = SLANG_GLSL; + nativeLanguage = SLANG_SOURCE_LANGUAGE_GLSL; + slangPassThrough = SLANG_PASS_THROUGH_GLSLANG; + profileName = "glsl_430"; + break; + + case RendererType::Vulkan: + renderer = createVKRenderer(); + slangTarget = SLANG_SPIRV; + nativeLanguage = SLANG_SOURCE_LANGUAGE_GLSL; + slangPassThrough = SLANG_PASS_THROUGH_GLSLANG; + profileName = "glsl_430"; + break; + + default: + fprintf(stderr, "error: unexpected\n"); + return SLANG_FAIL; + } + + if (!renderer) + { + fprintf(stderr, "Unable to create renderer\n"); + return SLANG_FAIL; + } + + Renderer::Desc desc; + desc.width = gWindowWidth; + desc.height = gWindowHeight; + + { + SlangResult res = renderer->initialize(desc, (HWND)window->getHandle()); + if (SLANG_FAILED(res)) + { + fprintf(stderr, "Unable to initialize renderer\n"); + return res; + } + } + + ShaderCompiler shaderCompiler; + shaderCompiler.renderer = renderer; + shaderCompiler.target = slangTarget; + shaderCompiler.profile = profileName; + shaderCompiler.slangSession = session; + + switch (gOptions.inputLanguageID) + { + case Options::InputLanguageID::Slang: + shaderCompiler.sourceLanguage = SLANG_SOURCE_LANGUAGE_SLANG; + shaderCompiler.passThrough = SLANG_PASS_THROUGH_NONE; + break; + + case Options::InputLanguageID::Native: + shaderCompiler.sourceLanguage = nativeLanguage; + shaderCompiler.passThrough = slangPassThrough; + break; + + default: + break; + } + + { + RenderTestApp app; + + SLANG_RETURN_ON_FAIL(app.initialize(renderer, &shaderCompiler)); + + window->show(); + + // ... and enter the event loop: + for (;;) + { + MSG message; + + int result = PeekMessageW(&message, NULL, 0, 0, PM_REMOVE); + if (result != 0) + { + if (message.message == WM_QUIT) + { + return (int)message.wParam; + } + + TranslateMessage(&message); + DispatchMessageW(&message); + } + else + { + // Whenever we don't have Windows events to process, we render a frame. + if (gOptions.shaderType == Options::ShaderProgramType::Compute) + { + app.runCompute(); + } + else + { + static const float kClearColor[] = { 0.25, 0.25, 0.25, 1.0 }; + renderer->setClearColor(kClearColor); + renderer->clearFrame(); + + app.renderFrame(); + } + // If we are in a mode where output is requested, we need to snapshot the back buffer here + if (gOptions.outputPath) + { + // Submit the work + renderer->submitGpuWork(); + // Wait until everything is complete + renderer->waitForGpu(); + + if (gOptions.shaderType == Options::ShaderProgramType::Compute || gOptions.shaderType == Options::ShaderProgramType::GraphicsCompute) + { + SLANG_RETURN_ON_FAIL(app.writeBindingOutput(gOptions.outputPath)); + } + else + { + SlangResult res = app.writeScreen(gOptions.outputPath); + + if (SLANG_FAILED(res)) + { + fprintf(stderr, "ERROR: failed to write screen capture to file\n"); + return res; + } + } + return SLANG_OK; + } + + renderer->presentFrame(); + } + } + } + + return SLANG_OK; +} + + +int main(int argc, char** argv) +{ + SlangSession* session = spCreateSession(nullptr); + SlangResult res = innerMain(Slang::StdWriters::initDefault(), session, argc, argv); + spDestroySession(session); + + return SLANG_FAILED(res) ? 1 : 0; +} + diff --git a/tools/render-test/render-test-tool.vcxproj b/tools/render-test/render-test-tool.vcxproj index 811ffa56d..dea8627e3 100644 --- a/tools/render-test/render-test-tool.vcxproj +++ b/tools/render-test/render-test-tool.vcxproj @@ -186,9 +186,9 @@ - + diff --git a/tools/render-test/render-test-tool.vcxproj.filters b/tools/render-test/render-test-tool.vcxproj.filters index ff3d52a7e..39197c7e9 100644 --- a/tools/render-test/render-test-tool.vcxproj.filters +++ b/tools/render-test/render-test-tool.vcxproj.filters @@ -26,15 +26,15 @@ - - Source Files - Source Files Source Files + + Source Files + Source Files diff --git a/tools/slang-reflection-test/main.cpp b/tools/slang-reflection-test/main.cpp deleted file mode 100644 index 04818dfdf..000000000 --- a/tools/slang-reflection-test/main.cpp +++ /dev/null @@ -1,935 +0,0 @@ -// main.cpp - -#include -#include -#include -#include - -#include -#include - -#include "../../source/core/slang-test-tool-util.h" - -struct PrettyWriter -{ - bool startOfLine = true; - int indent = 0; -}; - -static void writeRaw(PrettyWriter& writer, char const* begin, char const* end) -{ - SLANG_ASSERT(end >= begin); - Slang::StdWriters::getOut().write(begin, size_t(end - begin)); -} - -static void writeRaw(PrettyWriter& writer, char const* begin) -{ - writeRaw(writer, begin, begin + strlen(begin)); -} - -static void writeRawChar(PrettyWriter& writer, int c) -{ - char buffer[] = { (char) c, 0 }; - writeRaw(writer, buffer, buffer + 1); -} - -static void adjust(PrettyWriter& writer) -{ - if (!writer.startOfLine) - return; - - int indent = writer.indent; - for (int ii = 0; ii < indent; ++ii) - writeRaw(writer, " "); - - writer.startOfLine = false; -} - -static void indent(PrettyWriter& writer) -{ - writer.indent++; -} - -static void dedent(PrettyWriter& writer) -{ - writer.indent--; -} - -static void write(PrettyWriter& writer, char const* text) -{ - // TODO: can do this more efficiently... - char const* cursor = text; - for(;;) - { - char c = *cursor++; - if (!c) break; - - if (c == '\n') - { - writer.startOfLine = true; - } - else - { - adjust(writer); - } - - writeRawChar(writer, c); - } -} - -static void write(PrettyWriter& writer, SlangUInt val) -{ - adjust(writer); - Slang::StdWriters::getOut().print("%llu", (unsigned long long)val); -} - -static void emitReflectionVarInfoJSON(PrettyWriter& writer, slang::VariableReflection* var); -static void emitReflectionTypeLayoutJSON(PrettyWriter& writer, slang::TypeLayoutReflection* type); -static void emitReflectionTypeJSON(PrettyWriter& writer, slang::TypeReflection* type); - -static void emitReflectionVarBindingInfoJSON( - PrettyWriter& writer, - SlangParameterCategory category, - SlangUInt index, - SlangUInt count, - SlangUInt space = 0) -{ - if( category == SLANG_PARAMETER_CATEGORY_UNIFORM ) - { - write(writer,"\"kind\": \"uniform\""); - write(writer, ", "); - write(writer,"\"offset\": "); - write(writer, index); - write(writer, ", "); - write(writer, "\"size\": "); - write(writer, count); - } - else - { - write(writer, "\"kind\": \""); - switch( category ) - { - #define CASE(NAME, KIND) case SLANG_PARAMETER_CATEGORY_##NAME: write(writer, #KIND); break - CASE(CONSTANT_BUFFER, constantBuffer); - CASE(SHADER_RESOURCE, shaderResource); - CASE(UNORDERED_ACCESS, unorderedAccess); - CASE(VARYING_INPUT, varyingInput); - CASE(VARYING_OUTPUT, varyingOutput); - CASE(SAMPLER_STATE, samplerState); - CASE(UNIFORM, uniform); - CASE(PUSH_CONSTANT_BUFFER, pushConstantBuffer); - CASE(DESCRIPTOR_TABLE_SLOT, descriptorTableSlot); - CASE(SPECIALIZATION_CONSTANT, specializationConstant); - CASE(MIXED, mixed); - CASE(REGISTER_SPACE, registerSpace); - CASE(GENERIC, generic); - #undef CASE - - default: - write(writer, "unknown"); - assert(!"unhandled case"); - break; - } - write(writer, "\""); - if( space && category != SLANG_PARAMETER_CATEGORY_REGISTER_SPACE) - { - write(writer, ", "); - write(writer, "\"space\": "); - write(writer, space); - } - write(writer, ", "); - write(writer, "\"index\": "); - write(writer, index); - if( count != 1) - { - write(writer, ", "); - write(writer, "\"count\": "); - if( count == SLANG_UNBOUNDED_SIZE ) - { - write(writer, "\"unbounded\""); - } - else - { - write(writer, count); - } - } - } -} - -static void emitReflectionVarBindingInfoJSON( - PrettyWriter& writer, - slang::VariableLayoutReflection* var) -{ - auto stage = var->getStage(); - if (stage != SLANG_STAGE_NONE) - { - write(writer, ",\n"); - char const* stageName = "UNKNOWN"; - switch (stage) - { - case SLANG_STAGE_VERTEX: stageName = "vertex"; break; - case SLANG_STAGE_HULL: stageName = "hull"; break; - case SLANG_STAGE_DOMAIN: stageName = "domain"; break; - case SLANG_STAGE_GEOMETRY: stageName = "geometry"; break; - case SLANG_STAGE_FRAGMENT: stageName = "fragment"; break; - case SLANG_STAGE_COMPUTE: stageName = "compute"; break; - - default: - break; - } - - write(writer, "\"stage\": \""); - write(writer, stageName); - write(writer, "\""); - } - - auto typeLayout = var->getTypeLayout(); - auto categoryCount = var->getCategoryCount(); - - if (categoryCount) - { - write(writer, ",\n"); - if( categoryCount != 1 ) - { - write(writer,"\"bindings\": [\n"); - } - else - { - write(writer,"\"binding\": "); - } - indent(writer); - - for(uint32_t cc = 0; cc < categoryCount; ++cc ) - { - auto category = var->getCategoryByIndex(cc); - auto index = var->getOffset(category); - auto space = var->getBindingSpace(category); - auto count = typeLayout->getSize(category); - - if (cc != 0) write(writer, ",\n"); - - write(writer,"{"); - emitReflectionVarBindingInfoJSON( - writer, - category, - index, - count, - space); - write(writer,"}"); - } - - dedent(writer); - if( categoryCount != 1 ) - { - write(writer,"\n]"); - } - } - - if (auto semanticName = var->getSemanticName()) - { - write(writer, ",\n"); - write(writer,"\"semanticName\": \""); - write(writer, semanticName); - write(writer, "\""); - - if (auto semanticIndex = var->getSemanticIndex()) - { - write(writer, ",\n"); - write(writer,"\"semanticIndex\": "); - write(writer, semanticIndex); - } - } -} - -static void emitReflectionNameInfoJSON( - PrettyWriter& writer, - char const* name) -{ - // TODO: deal with escaping special characters if/when needed - write(writer, "\"name\": \""); - write(writer, name); - write(writer, "\""); -} - -static void emitReflectionModifierInfoJSON( - PrettyWriter& writer, - slang::VariableReflection* var) -{ - if( var->findModifier(slang::Modifier::Shared) ) - { - write(writer, ",\n\"shared\": true"); - } -} - -static void emitReflectionVarLayoutJSON( - PrettyWriter& writer, - slang::VariableLayoutReflection* var) -{ - write(writer, "{\n"); - indent(writer); - - emitReflectionNameInfoJSON(writer, var->getName()); - write(writer, ",\n"); - - write(writer, "\"type\": "); - emitReflectionTypeLayoutJSON(writer, var->getTypeLayout()); - - emitReflectionModifierInfoJSON(writer, var->getVariable()); - - emitReflectionVarBindingInfoJSON(writer, var); - - dedent(writer); - write(writer, "\n}"); -} - -static void emitReflectionScalarTypeInfoJSON( - PrettyWriter& writer, - SlangScalarType scalarType) -{ - write(writer, "\"scalarType\": \""); - switch (scalarType) - { - default: - write(writer, "unknown"); - assert(!"unhandled case"); - break; -#define CASE(TAG, ID) case slang::TypeReflection::ScalarType::TAG: write(writer, #ID); break - CASE(Void, void); - CASE(Bool, bool); - CASE(Int32, int32); - CASE(UInt32, uint32); - CASE(Int64, int64); - CASE(UInt64, uint64); - CASE(Float16, float16); - CASE(Float32, float32); - CASE(Float64, float64); -#undef CASE - } - write(writer, "\""); -} - -static void emitReflectionTypeInfoJSON( - PrettyWriter& writer, - slang::TypeReflection* type) -{ - auto kind = type->getKind(); - switch(kind) - { - case slang::TypeReflection::Kind::SamplerState: - write(writer, "\"kind\": \"samplerState\""); - break; - - case slang::TypeReflection::Kind::Resource: - { - auto shape = type->getResourceShape(); - auto access = type->getResourceAccess(); - write(writer, "\"kind\": \"resource\""); - write(writer, ",\n"); - write(writer, "\"baseShape\": \""); - switch (shape & SLANG_RESOURCE_BASE_SHAPE_MASK) - { - default: - write(writer, "unknown"); - assert(!"unhandled case"); - break; - -#define CASE(SHAPE, NAME) case SLANG_##SHAPE: write(writer, #NAME); break - CASE(TEXTURE_1D, texture1D); - CASE(TEXTURE_2D, texture2D); - CASE(TEXTURE_3D, texture3D); - CASE(TEXTURE_CUBE, textureCube); - CASE(TEXTURE_BUFFER, textureBuffer); - CASE(STRUCTURED_BUFFER, structuredBuffer); - CASE(BYTE_ADDRESS_BUFFER, byteAddressBuffer); -#undef CASE - } - write(writer, "\""); - if (shape & SLANG_TEXTURE_ARRAY_FLAG) - { - write(writer, ",\n"); - write(writer, "\"array\": true"); - } - if (shape & SLANG_TEXTURE_MULTISAMPLE_FLAG) - { - write(writer, ",\n"); - write(writer, "\"multisample\": true"); - } - - if( access != SLANG_RESOURCE_ACCESS_READ ) - { - write(writer, ",\n\"access\": \""); - switch(access) - { - default: - write(writer, "unknown"); - assert(!"unhandled case"); - break; - - case SLANG_RESOURCE_ACCESS_READ: - break; - - case SLANG_RESOURCE_ACCESS_READ_WRITE: write(writer, "readWrite"); break; - case SLANG_RESOURCE_ACCESS_RASTER_ORDERED: write(writer, "rasterOrdered"); break; - case SLANG_RESOURCE_ACCESS_APPEND: write(writer, "append"); break; - case SLANG_RESOURCE_ACCESS_CONSUME: write(writer, "consume"); break; - } - write(writer, "\""); - } - - // TODO: We should really print the result type for all resource - // types, but current test output depends on the old behavior, so - // we only add result type output for structured buffers at first. - // - switch (shape & SLANG_RESOURCE_BASE_SHAPE_MASK) - { - default: - break; - - case SLANG_STRUCTURED_BUFFER: - if( auto resultType = type->getResourceResultType() ) - { - write(writer, ",\n"); - write(writer, "\"resultType\": "); - emitReflectionTypeJSON( - writer, - resultType); - } - break; - } - } - break; - - case slang::TypeReflection::Kind::ConstantBuffer: - write(writer, "\"kind\": \"constantBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::ParameterBlock: - write(writer, "\"kind\": \"parameterBlock\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::TextureBuffer: - write(writer, "\"kind\": \"textureBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::ShaderStorageBuffer: - write(writer, "\"kind\": \"shaderStorageBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::Scalar: - write(writer, "\"kind\": \"scalar\""); - write(writer, ",\n"); - emitReflectionScalarTypeInfoJSON( - writer, - type->getScalarType()); - break; - - case slang::TypeReflection::Kind::Vector: - write(writer, "\"kind\": \"vector\""); - write(writer, ",\n"); - write(writer, "\"elementCount\": "); - write(writer, type->getElementCount()); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::Matrix: - write(writer, "\"kind\": \"matrix\""); - write(writer, ",\n"); - write(writer, "\"rowCount\": "); - write(writer, type->getRowCount()); - write(writer, ",\n"); - write(writer, "\"columnCount\": "); - write(writer, type->getColumnCount()); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON( - writer, - type->getElementType()); - break; - - case slang::TypeReflection::Kind::Array: - { - auto arrayType = type; - write(writer, "\"kind\": \"array\""); - write(writer, ",\n"); - write(writer, "\"elementCount\": "); - write(writer, arrayType->getElementCount()); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeJSON(writer, arrayType->getElementType()); - } - break; - - case slang::TypeReflection::Kind::Struct: - { - write(writer, "\"kind\": \"struct\",\n"); - write(writer, "\"fields\": [\n"); - indent(writer); - - auto structType = type; - auto fieldCount = structType->getFieldCount(); - for( uint32_t ff = 0; ff < fieldCount; ++ff ) - { - if (ff != 0) write(writer, ",\n"); - emitReflectionVarInfoJSON( - writer, - structType->getFieldByIndex(ff)); - } - dedent(writer); - write(writer, "\n]"); - } - break; - - case slang::TypeReflection::Kind::GenericTypeParameter: - write(writer, "\"kind\": \"GenericTypeParameter\",\n"); - emitReflectionNameInfoJSON(writer, type->getName()); - break; - case slang::TypeReflection::Kind::Interface: - write(writer, "\"kind\": \"Interface\",\n"); - emitReflectionNameInfoJSON(writer, type->getName()); - break; - default: - assert(!"unhandled case"); - break; - } -} - -static void emitReflectionTypeLayoutInfoJSON( - PrettyWriter& writer, - slang::TypeLayoutReflection* typeLayout) -{ - switch( typeLayout->getKind() ) - { - default: - emitReflectionTypeInfoJSON(writer, typeLayout->getType()); - break; - - case slang::TypeReflection::Kind::Array: - { - auto arrayTypeLayout = typeLayout; - auto elementTypeLayout = arrayTypeLayout->getElementTypeLayout(); - write(writer, "\"kind\": \"array\""); - write(writer, ",\n"); - write(writer, "\"elementCount\": "); - write(writer, arrayTypeLayout->getElementCount()); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeLayoutJSON( - writer, - elementTypeLayout); - if (arrayTypeLayout->getSize(SLANG_PARAMETER_CATEGORY_UNIFORM) != 0) - { - write(writer, ",\n"); - write(writer, "\"uniformStride\": "); - write(writer, arrayTypeLayout->getElementStride(SLANG_PARAMETER_CATEGORY_UNIFORM)); - } - } - break; - - case slang::TypeReflection::Kind::Struct: - { - auto structTypeLayout = typeLayout; - - write(writer, "\"kind\": \"struct\",\n"); - if( auto name = structTypeLayout->getName() ) - { - emitReflectionNameInfoJSON(writer, structTypeLayout->getName()); - write(writer, ",\n"); - } - write(writer, "\"fields\": [\n"); - indent(writer); - - auto fieldCount = structTypeLayout->getFieldCount(); - for( uint32_t ff = 0; ff < fieldCount; ++ff ) - { - if (ff != 0) write(writer, ",\n"); - emitReflectionVarLayoutJSON( - writer, - structTypeLayout->getFieldByIndex(ff)); - } - dedent(writer); - write(writer, "\n]"); - } - break; - - case slang::TypeReflection::Kind::ConstantBuffer: - write(writer, "\"kind\": \"constantBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeLayoutJSON( - writer, - typeLayout->getElementTypeLayout()); - break; - - case slang::TypeReflection::Kind::ParameterBlock: - write(writer, "\"kind\": \"parameterBlock\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeLayoutJSON( - writer, - typeLayout->getElementTypeLayout()); - break; - - case slang::TypeReflection::Kind::TextureBuffer: - write(writer, "\"kind\": \"textureBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeLayoutJSON( - writer, - typeLayout->getElementTypeLayout()); - break; - - case slang::TypeReflection::Kind::ShaderStorageBuffer: - write(writer, "\"kind\": \"shaderStorageBuffer\""); - write(writer, ",\n"); - write(writer, "\"elementType\": "); - emitReflectionTypeLayoutJSON( - writer, - typeLayout->getElementTypeLayout()); - break; - case slang::TypeReflection::Kind::GenericTypeParameter: - write(writer, "\"kind\": \"GenericTypeParameter\""); - write(writer, ",\n"); - emitReflectionNameInfoJSON(writer, typeLayout->getName()); - break; - case slang::TypeReflection::Kind::Interface: - write(writer, "\"kind\": \"Interface\",\n"); - write(writer, ",\n"); - emitReflectionNameInfoJSON(writer, typeLayout->getName()); - break; - } - - // TODO: emit size info for types -} - -static void emitReflectionTypeLayoutJSON( - PrettyWriter& writer, - slang::TypeLayoutReflection* typeLayout) -{ - write(writer, "{\n"); - indent(writer); - emitReflectionTypeLayoutInfoJSON(writer, typeLayout); - dedent(writer); - write(writer, "\n}"); -} - -static void emitReflectionTypeJSON( - PrettyWriter& writer, - slang::TypeReflection* type) -{ - write(writer, "{\n"); - indent(writer); - emitReflectionTypeInfoJSON(writer, type); - dedent(writer); - write(writer, "\n}"); -} - -static void emitReflectionVarInfoJSON( - PrettyWriter& writer, - slang::VariableReflection* var) -{ - emitReflectionNameInfoJSON(writer, var->getName()); - - emitReflectionModifierInfoJSON(writer, var); - - write(writer, ",\n"); - write(writer, "\"type\": "); - emitReflectionTypeJSON(writer, var->getType()); -} - -static void emitReflectionParamJSON( - PrettyWriter& writer, - slang::VariableLayoutReflection* param) -{ - write(writer, "{\n"); - indent(writer); - - emitReflectionNameInfoJSON(writer, param->getName()); - - emitReflectionModifierInfoJSON(writer, param->getVariable()); - - emitReflectionVarBindingInfoJSON(writer, param); - write(writer, ",\n"); - - write(writer, "\"type\": "); - emitReflectionTypeLayoutJSON(writer, param->getTypeLayout()); - - dedent(writer); - write(writer, "\n}"); -} - -template -struct Range -{ -public: - Range( - T begin, - T end) - : mBegin(begin) - , mEnd(end) - {} - - struct Iterator - { - public: - explicit Iterator(T value) - : mValue(value) - {} - - T operator*() const { return mValue; } - void operator++() { mValue++; } - - bool operator!=(Iterator const& other) - { - return mValue != other.mValue; - } - - private: - T mValue; - }; - - Iterator begin() const { return Iterator(mBegin); } - Iterator end() const { return Iterator(mEnd); } - -private: - T mBegin; - T mEnd; -}; - -template -Range range(T begin, T end) -{ - return Range(begin, end); -} - -template -Range range(T end) -{ - return Range(T(0), end); -} - -static void emitReflectionTypeParamJSON( - PrettyWriter& writer, - slang::TypeParameterReflection* typeParam) -{ - write(writer, "{\n"); - indent(writer); - emitReflectionNameInfoJSON(writer, typeParam->getName()); - write(writer, ",\n"); - write(writer, "constraints: \n"); - write(writer, "[\n"); - indent(writer); - auto constraintCount = typeParam->getConstraintCount(); - for (auto ee : range(constraintCount)) - { - if (ee != 0) write(writer, ",\n"); - write(writer, "{\n"); - indent(writer); - emitReflectionTypeInfoJSON(writer, typeParam->getConstraintByIndex(ee)); - dedent(writer); - write(writer, "\n}"); - } - dedent(writer); - write(writer, "\n]"); - dedent(writer); - write(writer, "\n}"); -} - -static void emitReflectionEntryPointJSON( - PrettyWriter& writer, - slang::EntryPointReflection* entryPoint) -{ - write(writer, "{\n"); - indent(writer); - - emitReflectionNameInfoJSON(writer, entryPoint->getName()); - - switch (entryPoint->getStage()) - { - case SLANG_STAGE_VERTEX: write(writer, ",\n\"stage:\": \"vertex\""); break; - case SLANG_STAGE_HULL: write(writer, ",\n\"stage:\": \"hull\""); break; - case SLANG_STAGE_DOMAIN: write(writer, ",\n\"stage:\": \"domain\""); break; - case SLANG_STAGE_GEOMETRY: write(writer, ",\n\"stage:\": \"geometry\""); break; - case SLANG_STAGE_FRAGMENT: write(writer, ",\n\"stage:\": \"fragment\""); break; - case SLANG_STAGE_COMPUTE: write(writer, ",\n\"stage:\": \"compute\""); break; - default: - break; - } - - auto parameterCount = entryPoint->getParameterCount(); - if (parameterCount) - { - write(writer, ",\n\"parameters\": [\n"); - indent(writer); - - for( auto pp : range(parameterCount) ) - { - if(pp != 0) write(writer, ",\n"); - - auto parameter = entryPoint->getParameterByIndex(pp); - emitReflectionParamJSON(writer, parameter); - } - - dedent(writer); - write(writer, "\n]"); - } - if (entryPoint->usesAnySampleRateInput()) - { - write(writer, ",\n\"usesAnySampleRateInput\": true"); - } - - if (entryPoint->getStage() == SLANG_STAGE_COMPUTE) - { - SlangUInt threadGroupSize[3]; - entryPoint->getComputeThreadGroupSize(3, threadGroupSize); - - write(writer, ",\n\"threadGroupSize\": ["); - for (int ii = 0; ii < 3; ++ii) - { - if (ii != 0) write(writer, ", "); - write(writer, threadGroupSize[ii]); - } - write(writer, "]"); - } - - dedent(writer); - write(writer, "\n}"); -} - -static void emitReflectionJSON( - PrettyWriter& writer, - slang::ShaderReflection* programReflection) -{ - write(writer, "{\n"); - indent(writer); - write(writer, "\"parameters\": [\n"); - indent(writer); - - auto parameterCount = programReflection->getParameterCount(); - for( auto pp : range(parameterCount) ) - { - if(pp != 0) write(writer, ",\n"); - - auto parameter = programReflection->getParameterByIndex(pp); - emitReflectionParamJSON(writer, parameter); - } - - dedent(writer); - write(writer, "\n]"); - - auto entryPointCount = programReflection->getEntryPointCount(); - if (entryPointCount) - { - write(writer, ",\n\"entryPoints\": [\n"); - indent(writer); - - for (auto ee : range(entryPointCount)) - { - if (ee != 0) write(writer, ",\n"); - - auto entryPoint = programReflection->getEntryPointByIndex(ee); - emitReflectionEntryPointJSON(writer, entryPoint); - } - - dedent(writer); - write(writer, "\n]"); - } - - auto genParamCount = programReflection->getTypeParameterCount(); - if (genParamCount) - { - write(writer, ",\n\"typeParams\":\n"); - write(writer, "[\n"); - indent(writer); - for (auto ee : range(genParamCount)) - { - if (ee != 0) write(writer, ",\n"); - - auto typeParam = programReflection->getTypeParameterByIndex(ee); - emitReflectionTypeParamJSON(writer, typeParam); - } - dedent(writer); - write(writer, "\n]"); - } - dedent(writer); - write(writer, "\n}\n"); -} - -void emitReflectionJSON( - SlangReflection* reflection) -{ - auto programReflection = (slang::ShaderReflection*) reflection; - - PrettyWriter writer; - - emitReflectionJSON(writer, programReflection); -} - -static SlangResult maybeDumpDiagnostic(SlangResult res, SlangCompileRequest* request) -{ - const char* diagnostic; - if (SLANG_FAILED(res) && (diagnostic = spGetDiagnosticOutput(request))) - { - Slang::StdWriters::getError().put(diagnostic); - } - return res; -} - -SLANG_TEST_TOOL_API SlangResult innerMain(Slang::StdWriters* stdWriters, SlangSession* session, int argc, const char*const* argv) -{ - Slang::StdWriters::setSingleton(stdWriters); - - SlangCompileRequest* request = spCreateCompileRequest(session); - - stdWriters->setRequestWriters(request); - - char const* appName = "slang-reflection-test"; - if (argc > 0) appName = argv[0]; - - SLANG_RETURN_ON_FAIL(maybeDumpDiagnostic(spProcessCommandLineArguments(request, &argv[1], argc - 1), request)); - SLANG_RETURN_ON_FAIL(maybeDumpDiagnostic(spCompile(request), request)); - - // Okay, let's go through and emit reflection info on whatever - // we have. - - SlangReflection* reflection = spGetReflection(request); - emitReflectionJSON(reflection); - - spDestroyCompileRequest(request); - - return SLANG_OK; -} - -int main( - int argc, - char** argv) -{ - SlangSession* session = spCreateSession(nullptr); - SlangResult res = innerMain(Slang::StdWriters::initDefault(), session, argc, argv); - spDestroySession(session); - - return SLANG_FAILED(res) ? 1 : 0; -} diff --git a/tools/slang-reflection-test/slang-reflection-test-main.cpp b/tools/slang-reflection-test/slang-reflection-test-main.cpp new file mode 100644 index 000000000..071a15cd4 --- /dev/null +++ b/tools/slang-reflection-test/slang-reflection-test-main.cpp @@ -0,0 +1,935 @@ +// slang-reflection-test-main.cpp + +#include +#include +#include +#include + +#include +#include + +#include "../../source/core/slang-test-tool-util.h" + +struct PrettyWriter +{ + bool startOfLine = true; + int indent = 0; +}; + +static void writeRaw(PrettyWriter& writer, char const* begin, char const* end) +{ + SLANG_ASSERT(end >= begin); + Slang::StdWriters::getOut().write(begin, size_t(end - begin)); +} + +static void writeRaw(PrettyWriter& writer, char const* begin) +{ + writeRaw(writer, begin, begin + strlen(begin)); +} + +static void writeRawChar(PrettyWriter& writer, int c) +{ + char buffer[] = { (char) c, 0 }; + writeRaw(writer, buffer, buffer + 1); +} + +static void adjust(PrettyWriter& writer) +{ + if (!writer.startOfLine) + return; + + int indent = writer.indent; + for (int ii = 0; ii < indent; ++ii) + writeRaw(writer, " "); + + writer.startOfLine = false; +} + +static void indent(PrettyWriter& writer) +{ + writer.indent++; +} + +static void dedent(PrettyWriter& writer) +{ + writer.indent--; +} + +static void write(PrettyWriter& writer, char const* text) +{ + // TODO: can do this more efficiently... + char const* cursor = text; + for(;;) + { + char c = *cursor++; + if (!c) break; + + if (c == '\n') + { + writer.startOfLine = true; + } + else + { + adjust(writer); + } + + writeRawChar(writer, c); + } +} + +static void write(PrettyWriter& writer, SlangUInt val) +{ + adjust(writer); + Slang::StdWriters::getOut().print("%llu", (unsigned long long)val); +} + +static void emitReflectionVarInfoJSON(PrettyWriter& writer, slang::VariableReflection* var); +static void emitReflectionTypeLayoutJSON(PrettyWriter& writer, slang::TypeLayoutReflection* type); +static void emitReflectionTypeJSON(PrettyWriter& writer, slang::TypeReflection* type); + +static void emitReflectionVarBindingInfoJSON( + PrettyWriter& writer, + SlangParameterCategory category, + SlangUInt index, + SlangUInt count, + SlangUInt space = 0) +{ + if( category == SLANG_PARAMETER_CATEGORY_UNIFORM ) + { + write(writer,"\"kind\": \"uniform\""); + write(writer, ", "); + write(writer,"\"offset\": "); + write(writer, index); + write(writer, ", "); + write(writer, "\"size\": "); + write(writer, count); + } + else + { + write(writer, "\"kind\": \""); + switch( category ) + { + #define CASE(NAME, KIND) case SLANG_PARAMETER_CATEGORY_##NAME: write(writer, #KIND); break + CASE(CONSTANT_BUFFER, constantBuffer); + CASE(SHADER_RESOURCE, shaderResource); + CASE(UNORDERED_ACCESS, unorderedAccess); + CASE(VARYING_INPUT, varyingInput); + CASE(VARYING_OUTPUT, varyingOutput); + CASE(SAMPLER_STATE, samplerState); + CASE(UNIFORM, uniform); + CASE(PUSH_CONSTANT_BUFFER, pushConstantBuffer); + CASE(DESCRIPTOR_TABLE_SLOT, descriptorTableSlot); + CASE(SPECIALIZATION_CONSTANT, specializationConstant); + CASE(MIXED, mixed); + CASE(REGISTER_SPACE, registerSpace); + CASE(GENERIC, generic); + #undef CASE + + default: + write(writer, "unknown"); + assert(!"unhandled case"); + break; + } + write(writer, "\""); + if( space && category != SLANG_PARAMETER_CATEGORY_REGISTER_SPACE) + { + write(writer, ", "); + write(writer, "\"space\": "); + write(writer, space); + } + write(writer, ", "); + write(writer, "\"index\": "); + write(writer, index); + if( count != 1) + { + write(writer, ", "); + write(writer, "\"count\": "); + if( count == SLANG_UNBOUNDED_SIZE ) + { + write(writer, "\"unbounded\""); + } + else + { + write(writer, count); + } + } + } +} + +static void emitReflectionVarBindingInfoJSON( + PrettyWriter& writer, + slang::VariableLayoutReflection* var) +{ + auto stage = var->getStage(); + if (stage != SLANG_STAGE_NONE) + { + write(writer, ",\n"); + char const* stageName = "UNKNOWN"; + switch (stage) + { + case SLANG_STAGE_VERTEX: stageName = "vertex"; break; + case SLANG_STAGE_HULL: stageName = "hull"; break; + case SLANG_STAGE_DOMAIN: stageName = "domain"; break; + case SLANG_STAGE_GEOMETRY: stageName = "geometry"; break; + case SLANG_STAGE_FRAGMENT: stageName = "fragment"; break; + case SLANG_STAGE_COMPUTE: stageName = "compute"; break; + + default: + break; + } + + write(writer, "\"stage\": \""); + write(writer, stageName); + write(writer, "\""); + } + + auto typeLayout = var->getTypeLayout(); + auto categoryCount = var->getCategoryCount(); + + if (categoryCount) + { + write(writer, ",\n"); + if( categoryCount != 1 ) + { + write(writer,"\"bindings\": [\n"); + } + else + { + write(writer,"\"binding\": "); + } + indent(writer); + + for(uint32_t cc = 0; cc < categoryCount; ++cc ) + { + auto category = var->getCategoryByIndex(cc); + auto index = var->getOffset(category); + auto space = var->getBindingSpace(category); + auto count = typeLayout->getSize(category); + + if (cc != 0) write(writer, ",\n"); + + write(writer,"{"); + emitReflectionVarBindingInfoJSON( + writer, + category, + index, + count, + space); + write(writer,"}"); + } + + dedent(writer); + if( categoryCount != 1 ) + { + write(writer,"\n]"); + } + } + + if (auto semanticName = var->getSemanticName()) + { + write(writer, ",\n"); + write(writer,"\"semanticName\": \""); + write(writer, semanticName); + write(writer, "\""); + + if (auto semanticIndex = var->getSemanticIndex()) + { + write(writer, ",\n"); + write(writer,"\"semanticIndex\": "); + write(writer, semanticIndex); + } + } +} + +static void emitReflectionNameInfoJSON( + PrettyWriter& writer, + char const* name) +{ + // TODO: deal with escaping special characters if/when needed + write(writer, "\"name\": \""); + write(writer, name); + write(writer, "\""); +} + +static void emitReflectionModifierInfoJSON( + PrettyWriter& writer, + slang::VariableReflection* var) +{ + if( var->findModifier(slang::Modifier::Shared) ) + { + write(writer, ",\n\"shared\": true"); + } +} + +static void emitReflectionVarLayoutJSON( + PrettyWriter& writer, + slang::VariableLayoutReflection* var) +{ + write(writer, "{\n"); + indent(writer); + + emitReflectionNameInfoJSON(writer, var->getName()); + write(writer, ",\n"); + + write(writer, "\"type\": "); + emitReflectionTypeLayoutJSON(writer, var->getTypeLayout()); + + emitReflectionModifierInfoJSON(writer, var->getVariable()); + + emitReflectionVarBindingInfoJSON(writer, var); + + dedent(writer); + write(writer, "\n}"); +} + +static void emitReflectionScalarTypeInfoJSON( + PrettyWriter& writer, + SlangScalarType scalarType) +{ + write(writer, "\"scalarType\": \""); + switch (scalarType) + { + default: + write(writer, "unknown"); + assert(!"unhandled case"); + break; +#define CASE(TAG, ID) case slang::TypeReflection::ScalarType::TAG: write(writer, #ID); break + CASE(Void, void); + CASE(Bool, bool); + CASE(Int32, int32); + CASE(UInt32, uint32); + CASE(Int64, int64); + CASE(UInt64, uint64); + CASE(Float16, float16); + CASE(Float32, float32); + CASE(Float64, float64); +#undef CASE + } + write(writer, "\""); +} + +static void emitReflectionTypeInfoJSON( + PrettyWriter& writer, + slang::TypeReflection* type) +{ + auto kind = type->getKind(); + switch(kind) + { + case slang::TypeReflection::Kind::SamplerState: + write(writer, "\"kind\": \"samplerState\""); + break; + + case slang::TypeReflection::Kind::Resource: + { + auto shape = type->getResourceShape(); + auto access = type->getResourceAccess(); + write(writer, "\"kind\": \"resource\""); + write(writer, ",\n"); + write(writer, "\"baseShape\": \""); + switch (shape & SLANG_RESOURCE_BASE_SHAPE_MASK) + { + default: + write(writer, "unknown"); + assert(!"unhandled case"); + break; + +#define CASE(SHAPE, NAME) case SLANG_##SHAPE: write(writer, #NAME); break + CASE(TEXTURE_1D, texture1D); + CASE(TEXTURE_2D, texture2D); + CASE(TEXTURE_3D, texture3D); + CASE(TEXTURE_CUBE, textureCube); + CASE(TEXTURE_BUFFER, textureBuffer); + CASE(STRUCTURED_BUFFER, structuredBuffer); + CASE(BYTE_ADDRESS_BUFFER, byteAddressBuffer); +#undef CASE + } + write(writer, "\""); + if (shape & SLANG_TEXTURE_ARRAY_FLAG) + { + write(writer, ",\n"); + write(writer, "\"array\": true"); + } + if (shape & SLANG_TEXTURE_MULTISAMPLE_FLAG) + { + write(writer, ",\n"); + write(writer, "\"multisample\": true"); + } + + if( access != SLANG_RESOURCE_ACCESS_READ ) + { + write(writer, ",\n\"access\": \""); + switch(access) + { + default: + write(writer, "unknown"); + assert(!"unhandled case"); + break; + + case SLANG_RESOURCE_ACCESS_READ: + break; + + case SLANG_RESOURCE_ACCESS_READ_WRITE: write(writer, "readWrite"); break; + case SLANG_RESOURCE_ACCESS_RASTER_ORDERED: write(writer, "rasterOrdered"); break; + case SLANG_RESOURCE_ACCESS_APPEND: write(writer, "append"); break; + case SLANG_RESOURCE_ACCESS_CONSUME: write(writer, "consume"); break; + } + write(writer, "\""); + } + + // TODO: We should really print the result type for all resource + // types, but current test output depends on the old behavior, so + // we only add result type output for structured buffers at first. + // + switch (shape & SLANG_RESOURCE_BASE_SHAPE_MASK) + { + default: + break; + + case SLANG_STRUCTURED_BUFFER: + if( auto resultType = type->getResourceResultType() ) + { + write(writer, ",\n"); + write(writer, "\"resultType\": "); + emitReflectionTypeJSON( + writer, + resultType); + } + break; + } + } + break; + + case slang::TypeReflection::Kind::ConstantBuffer: + write(writer, "\"kind\": \"constantBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::ParameterBlock: + write(writer, "\"kind\": \"parameterBlock\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::TextureBuffer: + write(writer, "\"kind\": \"textureBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::ShaderStorageBuffer: + write(writer, "\"kind\": \"shaderStorageBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::Scalar: + write(writer, "\"kind\": \"scalar\""); + write(writer, ",\n"); + emitReflectionScalarTypeInfoJSON( + writer, + type->getScalarType()); + break; + + case slang::TypeReflection::Kind::Vector: + write(writer, "\"kind\": \"vector\""); + write(writer, ",\n"); + write(writer, "\"elementCount\": "); + write(writer, type->getElementCount()); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::Matrix: + write(writer, "\"kind\": \"matrix\""); + write(writer, ",\n"); + write(writer, "\"rowCount\": "); + write(writer, type->getRowCount()); + write(writer, ",\n"); + write(writer, "\"columnCount\": "); + write(writer, type->getColumnCount()); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON( + writer, + type->getElementType()); + break; + + case slang::TypeReflection::Kind::Array: + { + auto arrayType = type; + write(writer, "\"kind\": \"array\""); + write(writer, ",\n"); + write(writer, "\"elementCount\": "); + write(writer, arrayType->getElementCount()); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeJSON(writer, arrayType->getElementType()); + } + break; + + case slang::TypeReflection::Kind::Struct: + { + write(writer, "\"kind\": \"struct\",\n"); + write(writer, "\"fields\": [\n"); + indent(writer); + + auto structType = type; + auto fieldCount = structType->getFieldCount(); + for( uint32_t ff = 0; ff < fieldCount; ++ff ) + { + if (ff != 0) write(writer, ",\n"); + emitReflectionVarInfoJSON( + writer, + structType->getFieldByIndex(ff)); + } + dedent(writer); + write(writer, "\n]"); + } + break; + + case slang::TypeReflection::Kind::GenericTypeParameter: + write(writer, "\"kind\": \"GenericTypeParameter\",\n"); + emitReflectionNameInfoJSON(writer, type->getName()); + break; + case slang::TypeReflection::Kind::Interface: + write(writer, "\"kind\": \"Interface\",\n"); + emitReflectionNameInfoJSON(writer, type->getName()); + break; + default: + assert(!"unhandled case"); + break; + } +} + +static void emitReflectionTypeLayoutInfoJSON( + PrettyWriter& writer, + slang::TypeLayoutReflection* typeLayout) +{ + switch( typeLayout->getKind() ) + { + default: + emitReflectionTypeInfoJSON(writer, typeLayout->getType()); + break; + + case slang::TypeReflection::Kind::Array: + { + auto arrayTypeLayout = typeLayout; + auto elementTypeLayout = arrayTypeLayout->getElementTypeLayout(); + write(writer, "\"kind\": \"array\""); + write(writer, ",\n"); + write(writer, "\"elementCount\": "); + write(writer, arrayTypeLayout->getElementCount()); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeLayoutJSON( + writer, + elementTypeLayout); + if (arrayTypeLayout->getSize(SLANG_PARAMETER_CATEGORY_UNIFORM) != 0) + { + write(writer, ",\n"); + write(writer, "\"uniformStride\": "); + write(writer, arrayTypeLayout->getElementStride(SLANG_PARAMETER_CATEGORY_UNIFORM)); + } + } + break; + + case slang::TypeReflection::Kind::Struct: + { + auto structTypeLayout = typeLayout; + + write(writer, "\"kind\": \"struct\",\n"); + if( auto name = structTypeLayout->getName() ) + { + emitReflectionNameInfoJSON(writer, structTypeLayout->getName()); + write(writer, ",\n"); + } + write(writer, "\"fields\": [\n"); + indent(writer); + + auto fieldCount = structTypeLayout->getFieldCount(); + for( uint32_t ff = 0; ff < fieldCount; ++ff ) + { + if (ff != 0) write(writer, ",\n"); + emitReflectionVarLayoutJSON( + writer, + structTypeLayout->getFieldByIndex(ff)); + } + dedent(writer); + write(writer, "\n]"); + } + break; + + case slang::TypeReflection::Kind::ConstantBuffer: + write(writer, "\"kind\": \"constantBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeLayoutJSON( + writer, + typeLayout->getElementTypeLayout()); + break; + + case slang::TypeReflection::Kind::ParameterBlock: + write(writer, "\"kind\": \"parameterBlock\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeLayoutJSON( + writer, + typeLayout->getElementTypeLayout()); + break; + + case slang::TypeReflection::Kind::TextureBuffer: + write(writer, "\"kind\": \"textureBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeLayoutJSON( + writer, + typeLayout->getElementTypeLayout()); + break; + + case slang::TypeReflection::Kind::ShaderStorageBuffer: + write(writer, "\"kind\": \"shaderStorageBuffer\""); + write(writer, ",\n"); + write(writer, "\"elementType\": "); + emitReflectionTypeLayoutJSON( + writer, + typeLayout->getElementTypeLayout()); + break; + case slang::TypeReflection::Kind::GenericTypeParameter: + write(writer, "\"kind\": \"GenericTypeParameter\""); + write(writer, ",\n"); + emitReflectionNameInfoJSON(writer, typeLayout->getName()); + break; + case slang::TypeReflection::Kind::Interface: + write(writer, "\"kind\": \"Interface\",\n"); + write(writer, ",\n"); + emitReflectionNameInfoJSON(writer, typeLayout->getName()); + break; + } + + // TODO: emit size info for types +} + +static void emitReflectionTypeLayoutJSON( + PrettyWriter& writer, + slang::TypeLayoutReflection* typeLayout) +{ + write(writer, "{\n"); + indent(writer); + emitReflectionTypeLayoutInfoJSON(writer, typeLayout); + dedent(writer); + write(writer, "\n}"); +} + +static void emitReflectionTypeJSON( + PrettyWriter& writer, + slang::TypeReflection* type) +{ + write(writer, "{\n"); + indent(writer); + emitReflectionTypeInfoJSON(writer, type); + dedent(writer); + write(writer, "\n}"); +} + +static void emitReflectionVarInfoJSON( + PrettyWriter& writer, + slang::VariableReflection* var) +{ + emitReflectionNameInfoJSON(writer, var->getName()); + + emitReflectionModifierInfoJSON(writer, var); + + write(writer, ",\n"); + write(writer, "\"type\": "); + emitReflectionTypeJSON(writer, var->getType()); +} + +static void emitReflectionParamJSON( + PrettyWriter& writer, + slang::VariableLayoutReflection* param) +{ + write(writer, "{\n"); + indent(writer); + + emitReflectionNameInfoJSON(writer, param->getName()); + + emitReflectionModifierInfoJSON(writer, param->getVariable()); + + emitReflectionVarBindingInfoJSON(writer, param); + write(writer, ",\n"); + + write(writer, "\"type\": "); + emitReflectionTypeLayoutJSON(writer, param->getTypeLayout()); + + dedent(writer); + write(writer, "\n}"); +} + +template +struct Range +{ +public: + Range( + T begin, + T end) + : mBegin(begin) + , mEnd(end) + {} + + struct Iterator + { + public: + explicit Iterator(T value) + : mValue(value) + {} + + T operator*() const { return mValue; } + void operator++() { mValue++; } + + bool operator!=(Iterator const& other) + { + return mValue != other.mValue; + } + + private: + T mValue; + }; + + Iterator begin() const { return Iterator(mBegin); } + Iterator end() const { return Iterator(mEnd); } + +private: + T mBegin; + T mEnd; +}; + +template +Range range(T begin, T end) +{ + return Range(begin, end); +} + +template +Range range(T end) +{ + return Range(T(0), end); +} + +static void emitReflectionTypeParamJSON( + PrettyWriter& writer, + slang::TypeParameterReflection* typeParam) +{ + write(writer, "{\n"); + indent(writer); + emitReflectionNameInfoJSON(writer, typeParam->getName()); + write(writer, ",\n"); + write(writer, "constraints: \n"); + write(writer, "[\n"); + indent(writer); + auto constraintCount = typeParam->getConstraintCount(); + for (auto ee : range(constraintCount)) + { + if (ee != 0) write(writer, ",\n"); + write(writer, "{\n"); + indent(writer); + emitReflectionTypeInfoJSON(writer, typeParam->getConstraintByIndex(ee)); + dedent(writer); + write(writer, "\n}"); + } + dedent(writer); + write(writer, "\n]"); + dedent(writer); + write(writer, "\n}"); +} + +static void emitReflectionEntryPointJSON( + PrettyWriter& writer, + slang::EntryPointReflection* entryPoint) +{ + write(writer, "{\n"); + indent(writer); + + emitReflectionNameInfoJSON(writer, entryPoint->getName()); + + switch (entryPoint->getStage()) + { + case SLANG_STAGE_VERTEX: write(writer, ",\n\"stage:\": \"vertex\""); break; + case SLANG_STAGE_HULL: write(writer, ",\n\"stage:\": \"hull\""); break; + case SLANG_STAGE_DOMAIN: write(writer, ",\n\"stage:\": \"domain\""); break; + case SLANG_STAGE_GEOMETRY: write(writer, ",\n\"stage:\": \"geometry\""); break; + case SLANG_STAGE_FRAGMENT: write(writer, ",\n\"stage:\": \"fragment\""); break; + case SLANG_STAGE_COMPUTE: write(writer, ",\n\"stage:\": \"compute\""); break; + default: + break; + } + + auto parameterCount = entryPoint->getParameterCount(); + if (parameterCount) + { + write(writer, ",\n\"parameters\": [\n"); + indent(writer); + + for( auto pp : range(parameterCount) ) + { + if(pp != 0) write(writer, ",\n"); + + auto parameter = entryPoint->getParameterByIndex(pp); + emitReflectionParamJSON(writer, parameter); + } + + dedent(writer); + write(writer, "\n]"); + } + if (entryPoint->usesAnySampleRateInput()) + { + write(writer, ",\n\"usesAnySampleRateInput\": true"); + } + + if (entryPoint->getStage() == SLANG_STAGE_COMPUTE) + { + SlangUInt threadGroupSize[3]; + entryPoint->getComputeThreadGroupSize(3, threadGroupSize); + + write(writer, ",\n\"threadGroupSize\": ["); + for (int ii = 0; ii < 3; ++ii) + { + if (ii != 0) write(writer, ", "); + write(writer, threadGroupSize[ii]); + } + write(writer, "]"); + } + + dedent(writer); + write(writer, "\n}"); +} + +static void emitReflectionJSON( + PrettyWriter& writer, + slang::ShaderReflection* programReflection) +{ + write(writer, "{\n"); + indent(writer); + write(writer, "\"parameters\": [\n"); + indent(writer); + + auto parameterCount = programReflection->getParameterCount(); + for( auto pp : range(parameterCount) ) + { + if(pp != 0) write(writer, ",\n"); + + auto parameter = programReflection->getParameterByIndex(pp); + emitReflectionParamJSON(writer, parameter); + } + + dedent(writer); + write(writer, "\n]"); + + auto entryPointCount = programReflection->getEntryPointCount(); + if (entryPointCount) + { + write(writer, ",\n\"entryPoints\": [\n"); + indent(writer); + + for (auto ee : range(entryPointCount)) + { + if (ee != 0) write(writer, ",\n"); + + auto entryPoint = programReflection->getEntryPointByIndex(ee); + emitReflectionEntryPointJSON(writer, entryPoint); + } + + dedent(writer); + write(writer, "\n]"); + } + + auto genParamCount = programReflection->getTypeParameterCount(); + if (genParamCount) + { + write(writer, ",\n\"typeParams\":\n"); + write(writer, "[\n"); + indent(writer); + for (auto ee : range(genParamCount)) + { + if (ee != 0) write(writer, ",\n"); + + auto typeParam = programReflection->getTypeParameterByIndex(ee); + emitReflectionTypeParamJSON(writer, typeParam); + } + dedent(writer); + write(writer, "\n]"); + } + dedent(writer); + write(writer, "\n}\n"); +} + +void emitReflectionJSON( + SlangReflection* reflection) +{ + auto programReflection = (slang::ShaderReflection*) reflection; + + PrettyWriter writer; + + emitReflectionJSON(writer, programReflection); +} + +static SlangResult maybeDumpDiagnostic(SlangResult res, SlangCompileRequest* request) +{ + const char* diagnostic; + if (SLANG_FAILED(res) && (diagnostic = spGetDiagnosticOutput(request))) + { + Slang::StdWriters::getError().put(diagnostic); + } + return res; +} + +SLANG_TEST_TOOL_API SlangResult innerMain(Slang::StdWriters* stdWriters, SlangSession* session, int argc, const char*const* argv) +{ + Slang::StdWriters::setSingleton(stdWriters); + + SlangCompileRequest* request = spCreateCompileRequest(session); + + stdWriters->setRequestWriters(request); + + char const* appName = "slang-reflection-test"; + if (argc > 0) appName = argv[0]; + + SLANG_RETURN_ON_FAIL(maybeDumpDiagnostic(spProcessCommandLineArguments(request, &argv[1], argc - 1), request)); + SLANG_RETURN_ON_FAIL(maybeDumpDiagnostic(spCompile(request), request)); + + // Okay, let's go through and emit reflection info on whatever + // we have. + + SlangReflection* reflection = spGetReflection(request); + emitReflectionJSON(reflection); + + spDestroyCompileRequest(request); + + return SLANG_OK; +} + +int main( + int argc, + char** argv) +{ + SlangSession* session = spCreateSession(nullptr); + SlangResult res = innerMain(Slang::StdWriters::initDefault(), session, argc, argv); + spDestroySession(session); + + return SLANG_FAILED(res) ? 1 : 0; +} diff --git a/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj b/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj index 77efa33f0..51018db26 100644 --- a/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj +++ b/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj @@ -166,7 +166,7 @@ - + diff --git a/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj.filters b/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj.filters index e9ae1c092..2e5dbea25 100644 --- a/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj.filters +++ b/tools/slang-reflection-test/slang-reflection-test-tool.vcxproj.filters @@ -6,7 +6,7 @@ - + Source Files diff --git a/tools/slang-test/main.cpp b/tools/slang-test/main.cpp deleted file mode 100644 index 102b0b961..000000000 --- a/tools/slang-test/main.cpp +++ /dev/null @@ -1,1905 +0,0 @@ -// main.cpp - -#include "../../source/core/slang-io.h" -#include "../../source/core/token-reader.h" -#include "../../source/core/slang-std-writers.h" - -#include "../../slang-com-helper.h" - -#include "../../source/core/slang-string-util.h" - -using namespace Slang; - -#include "os.h" -#include "render-api-util.h" -#include "test-context.h" -#include "test-reporter.h" -#include "options.h" -#include "slangc-tool.h" - -#define STB_IMAGE_IMPLEMENTATION -#include "external/stb/stb_image.h" - -#ifdef _WIN32 -#define SLANG_TEST_SUPPORT_HLSL 1 -#include -#endif - -#include -#include -#include -#include -#include - -// Options for a particular test -struct TestOptions -{ - String command; - List args; - - // The categories that this test was assigned to - List categories; -}; - -// Information on tests to run for a particular file -struct FileTestList -{ - List tests; -}; - -struct TestInput -{ - // Path to the input file for the test - String filePath; - - // Prefix for the path that test output should write to - // (usually the same as `filePath`, but will differ when - // we run multiple tests out of the same file) - String outputStem; - - // Arguments for the test (usually to be interpreted - // as command line args) - TestOptions const* testOptions; - - // The list of tests that will be run on this file - FileTestList const* testList; -}; - -typedef TestResult(*TestCallback)(TestContext* context, TestInput& input); - -// Globals - -/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ - -bool match(char const** ioCursor, char const* expected) -{ - char const* cursor = *ioCursor; - while(*expected && *cursor == *expected) - { - cursor++; - expected++; - } - if(*expected != 0) return false; - - *ioCursor = cursor; - return true; -} - -void skipHorizontalSpace(char const** ioCursor) -{ - char const* cursor = *ioCursor; - for( ;;) - { - switch( *cursor ) - { - case ' ': - case '\t': - cursor++; - continue; - - default: - break; - } - - break; - } - *ioCursor = cursor; -} - -void skipToEndOfLine(char const** ioCursor) -{ - char const* cursor = *ioCursor; - for( ;;) - { - int c = *cursor; - switch( c ) - { - default: - cursor++; - continue; - - case '\r': case '\n': - { - cursor++; - int d = *cursor; - if( (c ^ d) == ('\r' ^ '\n') ) - { - cursor++; - } - } - ; // fall through to: - case 0: - *ioCursor = cursor; - return; - } - } -} - -String getString(char const* textBegin, char const* textEnd) -{ - StringBuilder sb; - sb.Append(textBegin, textEnd - textBegin); - return sb.ProduceString(); -} - -String collectRestOfLine(char const** ioCursor) -{ - char const* cursor = *ioCursor; - - char const* textBegin = cursor; - skipToEndOfLine(&cursor); - char const* textEnd = cursor; - - *ioCursor = cursor; - return getString(textBegin, textEnd); -} - - - -TestResult gatherTestOptions( - TestCategorySet* categorySet, - char const** ioCursor, - FileTestList* testList) -{ - char const* cursor = *ioCursor; - - TestOptions testOptions; - - // Right after the `TEST` keyword, the user may specify - // one or more categories for the test. - if(*cursor == '(') - { - cursor++; - // optional test category - skipHorizontalSpace(&cursor); - char const* categoryStart = cursor; - for(;;) - { - switch( *cursor ) - { - default: - cursor++; - continue; - - case ',': - case ')': - { - char const* categoryEnd = cursor; - cursor++; - - auto categoryName = getString(categoryStart, categoryEnd); - TestCategory* category = categorySet->find(categoryName); - - if(!category) - { - return TestResult::Fail; - } - - - testOptions.categories.Add(category); - - if( *categoryEnd == ',' ) - { - skipHorizontalSpace(&cursor); - categoryStart = cursor; - continue; - } - } - break; - - case 0: case '\r': case '\n': - return TestResult::Fail; - } - - break; - } - } - - // If no categories were specified, then add the default category - if(testOptions.categories.Count() == 0) - { - testOptions.categories.Add(categorySet->defaultCategory); - } - - if(*cursor == ':') - cursor++; - else - { - return TestResult::Fail; - } - - // Next scan for a sub-command name - char const* commandStart = cursor; - for(;;) - { - switch(*cursor) - { - default: - cursor++; - continue; - - case ':': - break; - - case 0: case '\r': case '\n': - return TestResult::Fail; - } - - break; - } - char const* commandEnd = cursor; - - testOptions.command = getString(commandStart, commandEnd); - - if(*cursor == ':') - cursor++; - else - { - return TestResult::Fail; - } - - // Now scan for arguments. For now we just assume that - // any whitespace separation indicates a new argument - // (we don't support quoting) - for(;;) - { - skipHorizontalSpace(&cursor); - - // End of line? then no more options. - switch( *cursor ) - { - case 0: case '\r': case '\n': - skipToEndOfLine(&cursor); - testList->tests.Add(testOptions); - return TestResult::Pass; - - default: - break; - } - - // Let's try to read one option - char const* argBegin = cursor; - for(;;) - { - switch( *cursor ) - { - default: - cursor++; - continue; - - case 0: case '\r': case '\n': case ' ': case '\t': - break; - } - - break; - } - char const* argEnd = cursor; - assert(argBegin != argEnd); - - testOptions.args.Add(getString(argBegin, argEnd)); - } -} - -// Try to read command-line options from the test file itself -TestResult gatherTestsForFile( - TestCategorySet* categorySet, - String filePath, - FileTestList* testList) -{ - String fileContents; - try - { - fileContents = Slang::File::ReadAllText(filePath); - } - catch (Slang::IOException) - { - return TestResult::Fail; - } - - - // Walk through the lines of the file, looking for test commands - char const* cursor = fileContents.begin(); - - while(*cursor) - { - // We are at the start of a line of input. - - skipHorizontalSpace(&cursor); - - // Look for a pattern that matches what we want - if(match(&cursor, "//TEST_IGNORE_FILE")) - { - return TestResult::Ignored; - } - else if(match(&cursor, "//TEST")) - { - if(gatherTestOptions(categorySet, &cursor, testList) != TestResult::Pass) - return TestResult::Fail; - } - else - { - skipToEndOfLine(&cursor); - } - } - - return TestResult::Pass; -} - -OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpawner& spawner) -{ - const auto& options = context->options; - - if (!options.useExes) - { - String exeName = Path::GetFileNameWithoutEXT(spawner.executableName_); - - if (options.shouldBeVerbose) - { - StringBuilder builder; - - builder << "slang-test"; - - if (options.binDir) - { - builder << " -bindir " << options.binDir; - } - - builder << " " << exeName; - - // TODO(js): Potentially this should handle escaping parameters for the command line if need be - const auto& argList = spawner.argumentList_; - for (UInt i = 0; i < argList.Count(); ++i) - { - builder << " " << argList[i]; - } - - context->reporter->messageFormat(TestMessageType::Info, "%s\n", builder.begin()); - } - - auto func = context->getInnerMainFunc(String(context->options.binDir), exeName); - if (func) - { - StringBuilder stdErrorString; - StringBuilder stdOutString; - - // Say static so not released - StringWriter stdError(&stdErrorString, WriterFlag::IsConsole | WriterFlag::IsStatic); - StringWriter stdOut(&stdOutString, WriterFlag::IsConsole | WriterFlag::IsStatic); - - StdWriters* prevStdWriters = StdWriters::getSingleton(); - - StdWriters stdWriters; - stdWriters.setWriter(SLANG_WRITER_CHANNEL_STD_ERROR, &stdError); - stdWriters.setWriter(SLANG_WRITER_CHANNEL_STD_OUTPUT, &stdOut); - - if (exeName == "slangc") - { - stdWriters.setWriter(SLANG_WRITER_CHANNEL_DIAGNOSTIC, &stdError); - } - - List args; - args.Add(exeName.Buffer()); - for (int i = 0; i < int(spawner.argumentList_.Count()); ++i) - { - args.Add(spawner.argumentList_[i].Buffer()); - } - - SlangResult res = func(&stdWriters, context->getSession(), int(args.Count()), args.begin()); - - StdWriters::setSingleton(prevStdWriters); - - spawner.standardError_ = stdErrorString; - spawner.standardOutput_ = stdOutString; - - spawner.resultCode_ = TestToolUtil::getReturnCode(res); - - return kOSError_None; - } - } - - if (options.shouldBeVerbose) - { - String commandLine = spawner.getCommandLine(); - context->reporter->messageFormat(TestMessageType::Info, "%s\n", commandLine.begin()); - } - - - OSError err = spawner.spawnAndWaitForCompletion(); - if (err != kOSError_None) - { -// fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); - context->reporter->messageFormat(TestMessageType::RunError, "failed to run test '%S'", testPath.ToWString().begin()); - } - return err; -} - -String getOutput(OSProcessSpawner& spawner) -{ - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOuptut = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - - // We construct a single output string that captures the results - StringBuilder actualOutputBuilder; - actualOutputBuilder.Append("result code = "); - actualOutputBuilder.Append(resultCode); - actualOutputBuilder.Append("\nstandard error = {\n"); - actualOutputBuilder.Append(standardError); - actualOutputBuilder.Append("}\nstandard output = {\n"); - actualOutputBuilder.Append(standardOuptut); - actualOutputBuilder.Append("}\n"); - - return actualOutputBuilder.ProduceString(); -} - -// Finds the specialized or default path for expected data for a test. -// If neither are found, will return an empty string -String findExpectedPath(const TestInput& input, const char* postFix) -{ - StringBuilder specializedBuf; - - // Try the specialized name first - specializedBuf << input.outputStem; - if (postFix) - { - specializedBuf << postFix; - } - if (File::Exists(specializedBuf)) - { - return specializedBuf; - } - - - // Try the default name - StringBuilder defaultBuf; - defaultBuf.Clear(); - defaultBuf << input.filePath; - if (postFix) - { - defaultBuf << postFix; - } - - if (File::Exists(defaultBuf)) - { - return defaultBuf; - } - - // Couldn't find either - printf("referenceOutput '%s' or '%s' not found.\n", defaultBuf.Buffer(), specializedBuf.Buffer()); - - return ""; -} - -TestResult runSimpleTest(TestContext* context, TestInput& input) -{ - // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect - - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - spawner.pushArgument(filePath999); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - String actualOutput = getOutput(spawner); - - String expectedOutputPath = outputStem + ".expected"; - String expectedOutput; - try - { - expectedOutput = Slang::File::ReadAllText(expectedOutputPath); - } - catch (Slang::IOException) - { - } - - // If no expected output file was found, then we - // expect everything to be empty - if (expectedOutput.Length() == 0) - { - expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; - } - - TestResult result = TestResult::Pass; - - // Otherwise we compare to the expected output - if (actualOutput != expectedOutput) - { - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - result = TestResult::Fail; - } - - // If the test failed, then we write the actual output to a file - // so that we can easily diff it from the command line and - // diagnose the problem. - if (result == TestResult::Fail) - { - String actualOutputPath = outputStem + ".actual"; - Slang::File::WriteAllText(actualOutputPath, actualOutput); - - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - } - - return result; -} - -TestResult runReflectionTest(TestContext* context, TestInput& input) -{ - const auto& options = context->options; - auto filePath = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(options.binDir) + "slang-reflection-test" + osGetExecutableSuffix()); - spawner.pushArgument(filePath); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - String actualOutput = getOutput(spawner); - - String expectedOutputPath = outputStem + ".expected"; - String expectedOutput; - try - { - expectedOutput = Slang::File::ReadAllText(expectedOutputPath); - } - catch (Slang::IOException) - { - } - - // If no expected output file was found, then we - // expect everything to be empty - if (expectedOutput.Length() == 0) - { - expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; - } - - TestResult result = TestResult::Pass; - - // Otherwise we compare to the expected output - if (actualOutput != expectedOutput) - { - result = TestResult::Fail; - } - - // If the test failed, then we write the actual output to a file - // so that we can easily diff it from the command line and - // diagnose the problem. - if (result == TestResult::Fail) - { - String actualOutputPath = outputStem + ".actual"; - Slang::File::WriteAllText(actualOutputPath, actualOutput); - - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - } - - return result; -} - -String getExpectedOutput(String const& outputStem) -{ - String expectedOutputPath = outputStem + ".expected"; - String expectedOutput; - try - { - expectedOutput = Slang::File::ReadAllText(expectedOutputPath); - } - catch (Slang::IOException) - { - } - - // If no expected output file was found, then we - // expect everything to be empty - if (expectedOutput.Length() == 0) - { - expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; - } - - return expectedOutput; -} - -static SlangCompileTarget _getCompileTarget(const UnownedStringSlice& name) -{ -#define CASE(NAME, TARGET) if(name == NAME) return SLANG_##TARGET; - - CASE("hlsl", HLSL) - CASE("glsl", GLSL) - CASE("dxbc", DXBC) - CASE("dxbc-assembly", DXBC_ASM) - CASE("dxbc-asm", DXBC_ASM) - CASE("spirv", SPIRV) - CASE("spirv-assembly", SPIRV_ASM) - CASE("spirv-asm", SPIRV_ASM) - CASE("dxil", DXIL) - CASE("dxil-assembly", DXIL_ASM) - CASE("dxil-asm", DXIL_ASM) -#undef CASE - - return SLANG_TARGET_UNKNOWN; -} - -TestResult runCrossCompilerTest(TestContext* context, TestInput& input) -{ - // need to execute the stand-alone Slang compiler on the file - // then on the same file + `.glsl` and compare output - - auto filePath = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner actualSpawner; - OSProcessSpawner expectedSpawner; - - actualSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - expectedSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - - actualSpawner.pushArgument(filePath); - - const auto& args = input.testOptions->args; - - const UInt targetIndex = args.IndexOf("-target"); - if (targetIndex != UInt(-1) && targetIndex + 1 < args.Count()) - { - SlangCompileTarget target = _getCompileTarget(args[targetIndex + 1].getUnownedSlice()); - - // Check the session supports it. If not we ignore it - if (SLANG_FAILED(spSessionCheckCompileTargetSupport(context->getSession(), target))) - { - return TestResult::Ignored; - } - - switch (target) - { - case SLANG_DXIL_ASM: - { - expectedSpawner.pushArgument(filePath + ".hlsl"); - expectedSpawner.pushArgument("-pass-through"); - expectedSpawner.pushArgument("dxc"); - break; - } - default: - { - expectedSpawner.pushArgument(filePath + ".glsl"); - expectedSpawner.pushArgument("-pass-through"); - expectedSpawner.pushArgument("glslang"); - break; - } - } - } - - for( auto arg : input.testOptions->args ) - { - actualSpawner.pushArgument(arg); - expectedSpawner.pushArgument(arg); - } - - if (spawnAndWait(context, outputStem, expectedSpawner) != kOSError_None) - { - return TestResult::Fail; - } - - String expectedOutput = getOutput(expectedSpawner); - String expectedOutputPath = outputStem + ".expected"; - try - { - Slang::File::WriteAllText(expectedOutputPath, expectedOutput); - } - catch (Slang::IOException) - { - return TestResult::Fail; - } - - if (spawnAndWait(context, outputStem, actualSpawner) != kOSError_None) - { - return TestResult::Fail; - } - String actualOutput = getOutput(actualSpawner); - - TestResult result = TestResult::Pass; - - // Otherwise we compare to the expected output - if (actualOutput != expectedOutput) - { - result = TestResult::Fail; - } - - // Always fail if the compilation produced a failure, just - // to catch situations where, e.g., command-line options parsing - // caused the same error in both the Slang and glslang cases. - // - if( actualSpawner.getResultCode() != 0 ) - { - result = TestResult::Fail; - } - - // If the test failed, then we write the actual output to a file - // so that we can easily diff it from the command line and - // diagnose the problem. - if (result == TestResult::Fail) - { - String actualOutputPath = outputStem + ".actual"; - Slang::File::WriteAllText(actualOutputPath, actualOutput); - - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - } - - return result; -} - - -#ifdef SLANG_TEST_SUPPORT_HLSL -TestResult generateHLSLBaseline(TestContext* context, TestInput& input) -{ - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner spawner; - spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - spawner.pushArgument(filePath999); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - spawner.pushArgument("-target"); - spawner.pushArgument("dxbc-assembly"); - spawner.pushArgument("-pass-through"); - spawner.pushArgument("fxc"); - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - String expectedOutput = getOutput(spawner); - String expectedOutputPath = outputStem + ".expected"; - try - { - Slang::File::WriteAllText(expectedOutputPath, expectedOutput); - } - catch (Slang::IOException) - { - return TestResult::Fail; - } - return TestResult::Pass; -} - -TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) -{ - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - // We will use the Microsoft compiler to generate out expected output here - String expectedOutputPath = outputStem + ".expected"; - - // Generate the expected output using standard HLSL compiler - generateHLSLBaseline(context, input); - - // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - spawner.pushArgument(filePath999); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - // TODO: The compiler should probably define this automatically... - spawner.pushArgument("-D"); - spawner.pushArgument("__SLANG__"); - - spawner.pushArgument("-target"); - spawner.pushArgument("dxbc-assembly"); - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - // We ignore output to stdout, and only worry about what the compiler - // wrote to stderr. - - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOutput = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - - // We construct a single output string that captures the results - StringBuilder actualOutputBuilder; - actualOutputBuilder.Append("result code = "); - actualOutputBuilder.Append(resultCode); - actualOutputBuilder.Append("\nstandard error = {\n"); - actualOutputBuilder.Append(standardError); - actualOutputBuilder.Append("}\nstandard output = {\n"); - actualOutputBuilder.Append(standardOutput); - actualOutputBuilder.Append("}\n"); - - String actualOutput = actualOutputBuilder.ProduceString(); - - String expectedOutput; - try - { - expectedOutput = Slang::File::ReadAllText(expectedOutputPath); - } - catch (Slang::IOException) - { - } - - TestResult result = TestResult::Pass; - - // If no expected output file was found, then we - // expect everything to be empty - if (expectedOutput.Length() == 0) - { - if (resultCode != 0) result = TestResult::Fail; - if (standardError.Length() != 0) result = TestResult::Fail; - if (standardOutput.Length() != 0) result = TestResult::Fail; - } - // Otherwise we compare to the expected output - else if (actualOutput != expectedOutput) - { - result = TestResult::Fail; - } - - // Always fail if the compilation produced a failure, just - // to catch situations where, e.g., command-line options parsing - // caused the same error in both the Slang and fxc cases. - // - if( resultCode != 0 ) - { - result = TestResult::Fail; - } - - // If the test failed, then we write the actual output to a file - // so that we can easily diff it from the command line and - // diagnose the problem. - if (result == TestResult::Fail) - { - String actualOutputPath = outputStem + ".actual"; - Slang::File::WriteAllText(actualOutputPath, actualOutput); - - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - } - - return result; -} -#endif - -TestResult doGLSLComparisonTestRun(TestContext* context, - TestInput& input, - char const* langDefine, - char const* passThrough, - char const* outputKind, - String* outOutput) -{ - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); - spawner.pushArgument(filePath999); - - if( langDefine ) - { - spawner.pushArgument("-D"); - spawner.pushArgument(langDefine); - } - - if( passThrough ) - { - spawner.pushArgument("-pass-through"); - spawner.pushArgument(passThrough); - } - - spawner.pushArgument("-target"); - spawner.pushArgument("spirv-assembly"); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOuptut = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - - // We construct a single output string that captures the results - StringBuilder outputBuilder; - outputBuilder.Append("result code = "); - outputBuilder.Append(resultCode); - outputBuilder.Append("\nstandard error = {\n"); - outputBuilder.Append(standardError); - outputBuilder.Append("}\nstandard output = {\n"); - outputBuilder.Append(standardOuptut); - outputBuilder.Append("}\n"); - - String outputPath = outputStem + outputKind; - String output = outputBuilder.ProduceString(); - - *outOutput = output; - - return TestResult::Pass; -} - -TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) -{ - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - String expectedOutput; - String actualOutput; - - TestResult hlslResult = doGLSLComparisonTestRun(context, input, "__GLSL__", "glslang", ".expected", &expectedOutput); - TestResult slangResult = doGLSLComparisonTestRun(context, input, "__SLANG__", nullptr, ".actual", &actualOutput); - - Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); - Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - - if( hlslResult == TestResult::Fail ) return TestResult::Fail; - if( slangResult == TestResult::Fail ) return TestResult::Fail; - - if (actualOutput != expectedOutput) - { - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - - return TestResult::Fail; - } - - return TestResult::Pass; -} - - -TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, const char *const* langOpts, size_t numLangOpts) -{ - // TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a false pass - auto filePath999 = input.filePath; - auto outputStem = input.outputStem; - - const String referenceOutput = findExpectedPath(input, ".expected.txt"); - if (referenceOutput.Length() <= 0) - { - return TestResult::Fail; - } - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); - spawner.pushArgument(filePath999); - - for (auto arg : input.testOptions->args) - { - spawner.pushArgument(arg); - } - - for (int i = 0; i < int(numLangOpts); ++i) - { - spawner.pushArgument(langOpts[i]); - } - spawner.pushArgument("-o"); - auto actualOutputFile = outputStem + ".actual.txt"; - spawner.pushArgument(actualOutputFile); - - // clear the stale actual output file first. This will allow us to detect error if render-test fails and outputs nothing. - File::WriteAllText(actualOutputFile, ""); - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - printf("error spawning render-test\n"); - return TestResult::Fail; - } - - auto actualOutput = getOutput(spawner); - auto expectedOutput = getExpectedOutput(outputStem); - if (actualOutput != expectedOutput) - { - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - - String actualOutputPath = outputStem + ".actual"; - Slang::File::WriteAllText(actualOutputPath, actualOutput); - - return TestResult::Fail; - } - - // check against reference output - if (!File::Exists(actualOutputFile)) - { - printf("render-test not producing expected outputs.\n"); - printf("render-test output:\n%s\n", actualOutput.Buffer()); - return TestResult::Fail; - } - if (!File::Exists(referenceOutput)) - { - printf("referenceOutput %s not found.\n", referenceOutput.Buffer()); - return TestResult::Fail; - } - auto actualOutputContent = File::ReadAllText(actualOutputFile); - auto actualProgramOutput = Split(actualOutputContent, '\n'); - auto referenceProgramOutput = Split(File::ReadAllText(referenceOutput), '\n'); - auto printOutput = [&]() - { - context->reporter->messageFormat(TestMessageType::TestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); - }; - if (actualProgramOutput.Count() < referenceProgramOutput.Count()) - { - printOutput(); - return TestResult::Fail; - } - for (int i = 0; i < (int)referenceProgramOutput.Count(); i++) - { - auto reference = String(referenceProgramOutput[i].Trim()); - auto actual = String(actualProgramOutput[i].Trim()); - if (actual != reference) - { - printOutput(); - return TestResult::Fail; - } - } - return TestResult::Pass; -} - -TestResult runSlangComputeComparisonTest(TestContext* context, TestInput& input) -{ - const char* langOpts[] = { "-slang", "-compute" }; - return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); -} - -TestResult runSlangComputeComparisonTestEx(TestContext* context, TestInput& input) -{ - return runComputeComparisonImpl(context, input, nullptr, 0); -} - -TestResult runHLSLComputeTest(TestContext* context, TestInput& input) -{ - const char* langOpts[] = { "--hlsl-rewrite", "-compute" }; - return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); -} - -TestResult runSlangRenderComputeComparisonTest(TestContext* context, TestInput& input) -{ - const char* langOpts[] = { "-slang", "-gcompute" }; - return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); -} - -TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, char const* langOption, char const* outputKind, String* outOutput) -{ - // TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a false pass - - auto filePath = input.filePath; - auto outputStem = input.outputStem; - - OSProcessSpawner spawner; - - spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); - spawner.pushArgument(filePath); - - for( auto arg : input.testOptions->args ) - { - spawner.pushArgument(arg); - } - - spawner.pushArgument(langOption); - spawner.pushArgument("-o"); - spawner.pushArgument(outputStem + outputKind + ".png"); - - if (spawnAndWait(context, outputStem, spawner) != kOSError_None) - { - return TestResult::Fail; - } - - OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); - - String standardOutput = spawner.getStandardOutput(); - String standardError = spawner.getStandardError(); - - // We construct a single output string that captures the results - StringBuilder outputBuilder; - outputBuilder.Append("result code = "); - outputBuilder.Append(resultCode); - outputBuilder.Append("\nstandard error = {\n"); - outputBuilder.Append(standardError); - outputBuilder.Append("}\nstandard output = {\n"); - outputBuilder.Append(standardOutput); - outputBuilder.Append("}\n"); - - String outputPath = outputStem + outputKind; - String output = outputBuilder.ProduceString(); - - *outOutput = output; - - return TestResult::Pass; -} - -class STBImage -{ -public: - typedef STBImage ThisType; - - /// Reset back to default initialized state (frees any image set) - void reset(); - /// True if rhs has same size and amount of channels - bool isComparable(const ThisType& rhs) const; - - /// The width in pixels - int getWidth() const { return m_width; } - /// The height in pixels - int getHeight() const { return m_height; } - /// The number of channels (typically held as bytes in order) - int getNumChannels() const { return m_numChannels; } - - /// Get the contained pixels, nullptr if nothing loaded - const unsigned char* getPixels() const { return m_pixels; } - unsigned char* getPixels() { return m_pixels; } - - /// Read an image with filename. SLANG_OK on success - SlangResult read(const char* filename); - - ~STBImage() { reset(); } - - int m_width = 0; - int m_height = 0; - int m_numChannels = 0; - unsigned char* m_pixels = nullptr; -}; - -void STBImage::reset() -{ - if (m_pixels) - { - stbi_image_free(m_pixels); - m_pixels = nullptr; - } - m_width = 0; - m_height = 0; - m_numChannels = 0; -} - -SlangResult STBImage::read(const char* filename) -{ - reset(); - - m_pixels = stbi_load(filename, &m_width, &m_height, &m_numChannels, 0); - if (!m_pixels) - { - return SLANG_FAIL; - } - return SLANG_OK; -} - -bool STBImage::isComparable(const ThisType& rhs) const -{ - return (this == &rhs) || - (m_width == rhs.m_width && m_height == rhs.m_height && m_numChannels == rhs.m_numChannels); -} - - -TestResult doImageComparison(TestContext* context, String const& filePath) -{ - auto reporter = context->reporter; - - // Allow a difference in the low bits of the 8-bit result, just to play it safe - static const int kAbsoluteDiffCutoff = 2; - - // Allow a relative 1% difference - static const float kRelativeDiffCutoff = 0.01f; - - String expectedPath = filePath + ".expected.png"; - String actualPath = filePath + ".actual.png"; - - STBImage expectedImage; - if (SLANG_FAILED(expectedImage.read(expectedPath.Buffer()))) - { - reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", expectedPath.Buffer()); - return TestResult::Fail; - } - - STBImage actualImage; - if (SLANG_FAILED(actualImage.read(actualPath.Buffer()))) - { - reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", actualPath.Buffer()); - return TestResult::Fail; - } - - if (!expectedImage.isComparable(actualImage)) - { - reporter->messageFormat(TestMessageType::TestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); - return TestResult::Fail; - } - - { - const unsigned char* expectedPixels = expectedImage.getPixels(); - const unsigned char* actualPixels = actualImage.getPixels(); - - const int height = actualImage.getHeight(); - const int width = actualImage.getWidth(); - const int numChannels = actualImage.getNumChannels(); - const int rowSize = width * numChannels; - - for (int y = 0; y < height; ++y) - { - for (int i = 0; i < rowSize; ++i) - { - int expectedVal = expectedPixels[i]; - int actualVal = actualPixels[i]; - - int absoluteDiff = actualVal - expectedVal; - if (absoluteDiff < 0) absoluteDiff = -absoluteDiff; - - if (absoluteDiff < kAbsoluteDiffCutoff) - { - // There might be a difference, but we'll consider it to be inside tolerance - continue; - } - - float relativeDiff = 0.0f; - if (expectedVal != 0) - { - relativeDiff = fabsf(float(actualVal) - float(expectedVal)) / float(expectedVal); - - if (relativeDiff < kRelativeDiffCutoff) - { - // relative difference was small enough - continue; - } - } - - // TODO: may need to do some local search sorts of things, to deal with - // cases where vertex shader results lead to rendering that is off - // by one pixel... - - const int x = i / numChannels; - const int channelIndex = i % numChannels; - - reporter->messageFormat(TestMessageType::TestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", - x, y, channelIndex, - expectedVal, - actualVal, - absoluteDiff, - relativeDiff); - - // There was a difference we couldn't excuse! - return TestResult::Fail; - } - - expectedPixels += rowSize; - actualPixels += rowSize; - } - } - - return TestResult::Pass; -} - -TestResult runHLSLRenderComparisonTestImpl( - TestContext* context, - TestInput& input, - char const* expectedArg, - char const* actualArg) -{ - auto filePath = input.filePath; - auto outputStem = input.outputStem; - - String expectedOutput; - String actualOutput; - - TestResult hlslResult = doRenderComparisonTestRun(context, input, expectedArg, ".expected", &expectedOutput); - TestResult slangResult = doRenderComparisonTestRun(context, input, actualArg, ".actual", &actualOutput); - - Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); - Slang::File::WriteAllText(outputStem + ".actual", actualOutput); - - if( hlslResult == TestResult::Fail ) return TestResult::Fail; - if( slangResult == TestResult::Fail ) return TestResult::Fail; - - if (actualOutput != expectedOutput) - { - context->reporter->dumpOutputDifference(expectedOutput, actualOutput); - - return TestResult::Fail; - } - - // Next do an image comparison on the expected output images! - - TestResult imageCompareResult = doImageComparison(context, outputStem); - if(imageCompareResult != TestResult::Pass) - return imageCompareResult; - - return TestResult::Pass; -} - -TestResult runHLSLRenderComparisonTest(TestContext* context, TestInput& input) -{ - return runHLSLRenderComparisonTestImpl(context, input, "-hlsl", "-slang"); -} - -TestResult runHLSLCrossCompileRenderComparisonTest(TestContext* context, TestInput& input) -{ - return runHLSLRenderComparisonTestImpl(context, input, "-slang", "-glsl-cross"); -} - -TestResult runHLSLAndGLSLRenderComparisonTest(TestContext* context, TestInput& input) -{ - return runHLSLRenderComparisonTestImpl(context, input, "-hlsl-rewrite", "-glsl-rewrite"); -} - -TestResult skipTest(TestContext* /* context */, TestInput& /*input*/) -{ - return TestResult::Ignored; -} - -static bool hasRenderOption(RenderApiType apiType, const List& options) -{ - const RenderApiUtil::Info& info = RenderApiUtil::getInfo(apiType); - - List namesList; - - for (UInt i = 0; i < options.Count(); ++i) - { - const String& option = options[i]; - - if (option.StartsWith("-")) - { - const UnownedStringSlice parameter(option.Buffer() + 1, option.Buffer() + option.Length()); - // See if we have a match - for (int j = 0; j < SLANG_COUNT_OF(RenderApiUtil::s_infos); j++) - { - const auto& apiInfo = RenderApiUtil::s_infos[j]; - const UnownedStringSlice names(info.names); - - if (names.indexOf(',') >= 0) - { - StringUtil::split(names, ',', namesList); - - if (namesList.IndexOf(parameter) != UInt(-1)) - { - return true; - } - } - else if (names == parameter) - { - return true; - } - } - } - } - - return false; -} - -bool hasRenderOption(RenderApiType apiType, const TestOptions& options) -{ - return hasRenderOption(apiType, options.args); -} - -bool hasRenderOption(RenderApiType apiType, const FileTestList& testList) -{ - const int numTests = int(testList.tests.Count()); - for (int i = 0; i < numTests; i++) - { - if (hasRenderOption(apiType, testList.tests[i].args)) - { - return true; - } - } - return false; -} - -bool isHLSLTest(const String& command) -{ - return command == "COMPARE_HLSL" || - command == "COMPARE_HLSL_RENDER" || - command == "COMPARE_HLSL_CROSS_COMPILE_RENDER" || - command == "COMPARE_HLSL_GLSL_RENDER"; -} - -bool isRenderTest(const String& command) -{ - return command == "COMPARE_COMPUTE" || - command == "COMPARE_COMPUTE_EX" || - command == "HLSL_COMPUTE" || - command == "COMPARE_RENDER_COMPUTE" || - command == "COMPARE_HLSL_RENDER" || - command == "COMPARE_HLSL_CROSS_COMPILE_RENDER" || - command == "COMPARE_HLSL_GLSL_RENDER"; -} - -static bool canIgnoreTestWithDisabledRenderer(const TestOptions& testOptions, const Options& appOptions) -{ - for (int i = 0; i < int(RenderApiType::CountOf); ++i) - { - RenderApiType apiType = RenderApiType(i); - RenderApiFlag::Enum apiFlag = RenderApiFlag::Enum(1 << i); - - if (hasRenderOption(apiType, testOptions) && (appOptions.enabledApis & apiFlag) == 0) - { - return true; - } - } - - return false; -} - -TestResult runTest( - TestContext* context, - String const& filePath, - String const& outputStem, - TestOptions const& testOptions, - FileTestList const& testList) -{ - // If this test can be ignored - if (canIgnoreTestWithDisabledRenderer(testOptions, context->options)) - { - return TestResult::Ignored; - } - - // based on command name, dispatch to an appropriate callback - struct TestCommands - { - char const* name; - TestCallback callback; - }; - - static const TestCommands kTestCommands[] = - { - { "SIMPLE", &runSimpleTest}, - { "REFLECTION", &runReflectionTest}, -#if SLANG_TEST_SUPPORT_HLSL - { "COMPARE_HLSL", &runHLSLComparisonTest}, - { "COMPARE_HLSL_RENDER", &runHLSLRenderComparisonTest}, - { "COMPARE_HLSL_CROSS_COMPILE_RENDER", &runHLSLCrossCompileRenderComparisonTest}, - { "COMPARE_HLSL_GLSL_RENDER", &runHLSLAndGLSLRenderComparisonTest }, - { "COMPARE_COMPUTE", runSlangComputeComparisonTest}, - { "COMPARE_COMPUTE_EX", runSlangComputeComparisonTestEx}, - { "HLSL_COMPUTE", runHLSLComputeTest}, - { "COMPARE_RENDER_COMPUTE", &runSlangRenderComputeComparisonTest }, - -#else - { "COMPARE_HLSL", &skipTest }, - { "COMPARE_HLSL_RENDER", &skipTest }, - { "COMPARE_HLSL_CROSS_COMPILE_RENDER", &skipTest}, - { "COMPARE_HLSL_GLSL_RENDER", &skipTest }, - { "COMPARE_COMPUTE", &skipTest}, - { "COMPARE_COMPUTE_EX", &skipTest}, - { "HLSL_COMPUTE", &skipTest}, - { "COMPARE_RENDER_COMPUTE", &skipTest }, -#endif - { "COMPARE_GLSL", &runGLSLComparisonTest }, - { "CROSS_COMPILE", &runCrossCompilerTest }, - { nullptr, nullptr }, - }; - - for( auto ii = kTestCommands; ii->name; ++ii ) - { - if(testOptions.command != ii->name) - continue; - - TestInput testInput; - testInput.filePath = filePath; - testInput.outputStem = outputStem; - testInput.testOptions = &testOptions; - testInput.testList = &testList; - - { - TestReporter* reporter = context->reporter; - TestReporter::TestScope scope(reporter, outputStem); - - TestResult testResult = ii->callback(context, testInput); - reporter->addResult(testResult); - - return testResult; - } - } - - // No actual test runner found! - - return TestResult::Fail; -} - -bool testCategoryMatches( - TestCategory* sub, - TestCategory* sup) -{ - auto ss = sub; - while(ss) - { - if(ss == sup) - return true; - - ss = ss->parent; - } - return false; -} - -bool testCategoryMatches( - TestCategory* categoryToMatch, - Dictionary categorySet) -{ - for( auto item : categorySet ) - { - if(testCategoryMatches(categoryToMatch, item.Value)) - return true; - } - return false; -} - -bool testPassesCategoryMask( - TestContext* context, - TestOptions const& test) -{ - // Don't include a test we should filter out - for( auto testCategory : test.categories ) - { - if(testCategoryMatches(testCategory, context->options.excludeCategories)) - return false; - } - - // Otherwise inclue any test the user asked for - for( auto testCategory : test.categories ) - { - if(testCategoryMatches(testCategory, context->options.includeCategories)) - return true; - } - - // skip by default - return false; -} - -void runTestsOnFile( - TestContext* context, - String filePath) -{ - // Gather a list of tests to run - FileTestList testList; - - if( gatherTestsForFile(&context->categorySet, filePath, &testList) == TestResult::Ignored ) - { - // Test was explicitly ignored - return; - } - - // Note cases where a test file exists, but we found nothing to run - if( testList.tests.Count() == 0 ) - { - context->reporter->addTest(filePath, TestResult::Ignored); - return; - } - - List synthesizedTests; - - // If dx12 is available synthesize Dx12 test - if ((context->options.synthesizedTestApis & RenderApiFlag::D3D12) != 0) - { - // If doesn't have option generate dx12 options from dx11 - if (!hasRenderOption(RenderApiType::D3D12, testList)) - { - const int numTests = int(testList.tests.Count()); - for (int i = 0; i < numTests; i++) - { - const TestOptions& testOptions = testList.tests[i]; - // If it's a render test, and there is on d3d option, add one - if (isRenderTest(testOptions.command) && !hasRenderOption(RenderApiType::D3D12, testOptions)) - { - // Add with -dx12 option - TestOptions testOptionsCopy(testOptions); - testOptionsCopy.args.Add("-dx12"); - - synthesizedTests.Add(testOptionsCopy); - } - } - } - } - - // If Vulkan is available synthesize Vulkan test - if ((context->options.synthesizedTestApis & RenderApiFlag::Vulkan) != 0) - { - // If doesn't have option generate dx12 options from dx11 - if (!hasRenderOption(RenderApiType::Vulkan, testList)) - { - const int numTests = int(testList.tests.Count()); - for (int i = 0; i < numTests; i++) - { - const TestOptions& testOptions = testList.tests[i]; - // If it's a render test, and there is on d3d option, add one - if (isRenderTest(testOptions.command) && !isHLSLTest(testOptions.command) && !hasRenderOption(RenderApiType::Vulkan, testOptions)) - { - // Add with -vk option - TestOptions testOptionsCopy(testOptions); - testOptionsCopy.args.Add("-vk"); - - UInt index = testOptionsCopy.args.IndexOf("-hlsl"); - if (index != UInt(-1)) - { - testOptionsCopy.args.RemoveAt(index); - } - - synthesizedTests.Add(testOptionsCopy); - } - } - } - } - - // Add any tests that were synthesized - for (UInt i = 0; i < synthesizedTests.Count(); ++i) - { - testList.tests.Add(synthesizedTests[i]); - } - - // We have found a test to run! - int subTestCount = 0; - for( auto& tt : testList.tests ) - { - int subTestIndex = subTestCount++; - - // Check that the test passes our current category mask - if(!testPassesCategoryMask(context, tt)) - { - continue; - } - - String outputStem = filePath; - if(subTestIndex != 0) - { - outputStem = outputStem + "." + String(subTestIndex); - } - - /* TestResult result = */ runTest(context, filePath, outputStem, tt, testList); - - // Could determine if to continue or not here... based on result - } -} - - -static bool endsWithAllowedExtension( - TestContext* /*context*/, - String filePath) -{ - char const* allowedExtensions[] = { - ".slang", - ".hlsl", - ".fx", - ".glsl", - ".vert", - ".frag", - ".geom", - ".tesc", - ".tese", - ".comp", - ".internal", - ".ahit", - ".chit", - ".miss", - ".rgen", - nullptr }; - - for( auto ii = allowedExtensions; *ii; ++ii ) - { - if(filePath.EndsWith(*ii)) - return true; - } - - return false; -} - -static bool shouldRunTest( - TestContext* context, - String filePath) -{ - if(!endsWithAllowedExtension(context, filePath)) - return false; - - if( context->options.testPrefix ) - { - if( strncmp(context->options.testPrefix, filePath.begin(), strlen(context->options.testPrefix)) != 0 ) - { - return false; - } - } - - return true; -} - -void runTestsInDirectory( - TestContext* context, - String directoryPath) -{ - for (auto file : osFindFilesInDirectory(directoryPath)) - { - if( shouldRunTest(context, file) ) - { -// fprintf(stderr, "slang-test: found '%s'\n", file.Buffer()); - runTestsOnFile(context, file); - } - } - for (auto subdir : osFindChildDirectories(directoryPath)) - { - runTestsInDirectory(context, subdir); - } -} - -SlangResult innerMain(int argc, char** argv) -{ - StdWriters::initDefault(); - - // The context holds useful things used during testing - TestContext context; - SLANG_RETURN_ON_FAIL(SLANG_FAILED(context.init())) - - auto& categorySet = context.categorySet; - - // Set up our test categories here - auto fullTestCategory = categorySet.add("full", nullptr); - auto quickTestCategory = categorySet.add("quick", fullTestCategory); - /*auto smokeTestCategory = */categorySet.add("smoke", quickTestCategory); - auto renderTestCategory = categorySet.add("render", fullTestCategory); - /*auto computeTestCategory = */categorySet.add("compute", fullTestCategory); - auto vulkanTestCategory = categorySet.add("vulkan", fullTestCategory); - auto unitTestCatagory = categorySet.add("unit-test", fullTestCategory); - auto compatibilityIssueCatagory = categorySet.add("compatibility-issue", fullTestCategory); - - // An un-categorized test will always belong to the `full` category - categorySet.defaultCategory = fullTestCategory; - - { - // We can set the slangc command line tool, to just use the function defined here - context.setInnerMainFunc("slangc", &SlangCTool::innerMain); - } - - SLANG_RETURN_ON_FAIL(Options::parse(argc, argv, &categorySet, StdWriters::getError(), &context.options)); - - Options& options = context.options; - - if (options.subCommand.Length()) - { - // Get the function from the tool - auto func = context.getInnerMainFunc(options.binDir, options.subCommand); - if (!func) - { - StdWriters::getError().print("error: Unable to launch tool '%s'\n", options.subCommand.Buffer()); - return SLANG_FAIL; - } - - // Copy args to a char* list - const auto& srcArgs = options.subCommandArgs; - List args; - args.SetSize(srcArgs.Count()); - for (UInt i = 0; i < srcArgs.Count(); ++i) - { - args[i] = srcArgs[i].Buffer(); - } - - return func(StdWriters::getSingleton(), context.getSession(), int(args.Count()), args.Buffer()); - } - - if( options.includeCategories.Count() == 0 ) - { - options.includeCategories.Add(fullTestCategory, fullTestCategory); - } - - // Exclude rendering tests when building under AppVeyor. - // - // TODO: this is very ad hoc, and we should do something cleaner. - if( options.outputMode == TestOutputMode::AppVeyor ) - { - options.excludeCategories.Add(renderTestCategory, renderTestCategory); - options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); - } - - { - // Setup the reporter - TestReporter reporter; - SLANG_RETURN_ON_FAIL(reporter.init(options.outputMode)); - - context.reporter = &reporter; - - reporter.m_dumpOutputOnFailure = options.dumpOutputOnFailure; - reporter.m_isVerbose = options.shouldBeVerbose; - - { - TestReporter::SuiteScope suiteScope(&reporter, "tests"); - // Enumerate test files according to policy - // TODO: add more directories to this list - // TODO: allow for a command-line argument to select a particular directory - runTestsInDirectory(&context, "tests/"); - } - - // Run the unit tests (these are internal C++ tests - not specified via files in a directory) - // They are registered with SLANG_UNIT_TEST macro - { - TestReporter::SuiteScope suiteScope(&reporter, "unit tests"); - TestReporter::set(&reporter); - - // Run the unit tests - TestRegister* cur = TestRegister::s_first; - while (cur) - { - StringBuilder filePath; - filePath << "unit-tests/" << cur->m_name << ".internal"; - - TestOptions testOptions; - testOptions.categories.Add(unitTestCatagory); - testOptions.command = filePath; - - if (shouldRunTest(&context, testOptions.command)) - { - if (testPassesCategoryMask(&context, testOptions)) - { - reporter.startTest(testOptions.command); - // Run the test function - cur->m_func(); - reporter.endTest(); - } - else - { - reporter.addTest(testOptions.command, TestResult::Ignored); - } - } - - // Next - cur = cur->m_next; - } - - TestReporter::set(nullptr); - } - - reporter.outputSummary(); - return reporter.didAllSucceed() ? SLANG_OK : SLANG_FAIL; - } -} - -int main(int argc, char** argv) -{ - const SlangResult res = innerMain(argc, argv); -#ifdef _MSC_VER - _CrtDumpMemoryLeaks(); -#endif - return SLANG_SUCCEEDED(res) ? 0 : 1; -} diff --git a/tools/slang-test/slang-test-main.cpp b/tools/slang-test/slang-test-main.cpp new file mode 100644 index 000000000..c0e5bd95b --- /dev/null +++ b/tools/slang-test/slang-test-main.cpp @@ -0,0 +1,1905 @@ +// slang-test-main.cpp + +#include "../../source/core/slang-io.h" +#include "../../source/core/token-reader.h" +#include "../../source/core/slang-std-writers.h" + +#include "../../slang-com-helper.h" + +#include "../../source/core/slang-string-util.h" + +using namespace Slang; + +#include "os.h" +#include "render-api-util.h" +#include "test-context.h" +#include "test-reporter.h" +#include "options.h" +#include "slangc-tool.h" + +#define STB_IMAGE_IMPLEMENTATION +#include "external/stb/stb_image.h" + +#ifdef _WIN32 +#define SLANG_TEST_SUPPORT_HLSL 1 +#include +#endif + +#include +#include +#include +#include +#include + +// Options for a particular test +struct TestOptions +{ + String command; + List args; + + // The categories that this test was assigned to + List categories; +}; + +// Information on tests to run for a particular file +struct FileTestList +{ + List tests; +}; + +struct TestInput +{ + // Path to the input file for the test + String filePath; + + // Prefix for the path that test output should write to + // (usually the same as `filePath`, but will differ when + // we run multiple tests out of the same file) + String outputStem; + + // Arguments for the test (usually to be interpreted + // as command line args) + TestOptions const* testOptions; + + // The list of tests that will be run on this file + FileTestList const* testList; +}; + +typedef TestResult(*TestCallback)(TestContext* context, TestInput& input); + +// Globals + +/* !!!!!!!!!!!!!!!!!!!!!!!!!!!!! Functions !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! */ + +bool match(char const** ioCursor, char const* expected) +{ + char const* cursor = *ioCursor; + while(*expected && *cursor == *expected) + { + cursor++; + expected++; + } + if(*expected != 0) return false; + + *ioCursor = cursor; + return true; +} + +void skipHorizontalSpace(char const** ioCursor) +{ + char const* cursor = *ioCursor; + for( ;;) + { + switch( *cursor ) + { + case ' ': + case '\t': + cursor++; + continue; + + default: + break; + } + + break; + } + *ioCursor = cursor; +} + +void skipToEndOfLine(char const** ioCursor) +{ + char const* cursor = *ioCursor; + for( ;;) + { + int c = *cursor; + switch( c ) + { + default: + cursor++; + continue; + + case '\r': case '\n': + { + cursor++; + int d = *cursor; + if( (c ^ d) == ('\r' ^ '\n') ) + { + cursor++; + } + } + ; // fall through to: + case 0: + *ioCursor = cursor; + return; + } + } +} + +String getString(char const* textBegin, char const* textEnd) +{ + StringBuilder sb; + sb.Append(textBegin, textEnd - textBegin); + return sb.ProduceString(); +} + +String collectRestOfLine(char const** ioCursor) +{ + char const* cursor = *ioCursor; + + char const* textBegin = cursor; + skipToEndOfLine(&cursor); + char const* textEnd = cursor; + + *ioCursor = cursor; + return getString(textBegin, textEnd); +} + + + +TestResult gatherTestOptions( + TestCategorySet* categorySet, + char const** ioCursor, + FileTestList* testList) +{ + char const* cursor = *ioCursor; + + TestOptions testOptions; + + // Right after the `TEST` keyword, the user may specify + // one or more categories for the test. + if(*cursor == '(') + { + cursor++; + // optional test category + skipHorizontalSpace(&cursor); + char const* categoryStart = cursor; + for(;;) + { + switch( *cursor ) + { + default: + cursor++; + continue; + + case ',': + case ')': + { + char const* categoryEnd = cursor; + cursor++; + + auto categoryName = getString(categoryStart, categoryEnd); + TestCategory* category = categorySet->find(categoryName); + + if(!category) + { + return TestResult::Fail; + } + + + testOptions.categories.Add(category); + + if( *categoryEnd == ',' ) + { + skipHorizontalSpace(&cursor); + categoryStart = cursor; + continue; + } + } + break; + + case 0: case '\r': case '\n': + return TestResult::Fail; + } + + break; + } + } + + // If no categories were specified, then add the default category + if(testOptions.categories.Count() == 0) + { + testOptions.categories.Add(categorySet->defaultCategory); + } + + if(*cursor == ':') + cursor++; + else + { + return TestResult::Fail; + } + + // Next scan for a sub-command name + char const* commandStart = cursor; + for(;;) + { + switch(*cursor) + { + default: + cursor++; + continue; + + case ':': + break; + + case 0: case '\r': case '\n': + return TestResult::Fail; + } + + break; + } + char const* commandEnd = cursor; + + testOptions.command = getString(commandStart, commandEnd); + + if(*cursor == ':') + cursor++; + else + { + return TestResult::Fail; + } + + // Now scan for arguments. For now we just assume that + // any whitespace separation indicates a new argument + // (we don't support quoting) + for(;;) + { + skipHorizontalSpace(&cursor); + + // End of line? then no more options. + switch( *cursor ) + { + case 0: case '\r': case '\n': + skipToEndOfLine(&cursor); + testList->tests.Add(testOptions); + return TestResult::Pass; + + default: + break; + } + + // Let's try to read one option + char const* argBegin = cursor; + for(;;) + { + switch( *cursor ) + { + default: + cursor++; + continue; + + case 0: case '\r': case '\n': case ' ': case '\t': + break; + } + + break; + } + char const* argEnd = cursor; + assert(argBegin != argEnd); + + testOptions.args.Add(getString(argBegin, argEnd)); + } +} + +// Try to read command-line options from the test file itself +TestResult gatherTestsForFile( + TestCategorySet* categorySet, + String filePath, + FileTestList* testList) +{ + String fileContents; + try + { + fileContents = Slang::File::ReadAllText(filePath); + } + catch (Slang::IOException) + { + return TestResult::Fail; + } + + + // Walk through the lines of the file, looking for test commands + char const* cursor = fileContents.begin(); + + while(*cursor) + { + // We are at the start of a line of input. + + skipHorizontalSpace(&cursor); + + // Look for a pattern that matches what we want + if(match(&cursor, "//TEST_IGNORE_FILE")) + { + return TestResult::Ignored; + } + else if(match(&cursor, "//TEST")) + { + if(gatherTestOptions(categorySet, &cursor, testList) != TestResult::Pass) + return TestResult::Fail; + } + else + { + skipToEndOfLine(&cursor); + } + } + + return TestResult::Pass; +} + +OSError spawnAndWait(TestContext* context, const String& testPath, OSProcessSpawner& spawner) +{ + const auto& options = context->options; + + if (!options.useExes) + { + String exeName = Path::GetFileNameWithoutEXT(spawner.executableName_); + + if (options.shouldBeVerbose) + { + StringBuilder builder; + + builder << "slang-test"; + + if (options.binDir) + { + builder << " -bindir " << options.binDir; + } + + builder << " " << exeName; + + // TODO(js): Potentially this should handle escaping parameters for the command line if need be + const auto& argList = spawner.argumentList_; + for (UInt i = 0; i < argList.Count(); ++i) + { + builder << " " << argList[i]; + } + + context->reporter->messageFormat(TestMessageType::Info, "%s\n", builder.begin()); + } + + auto func = context->getInnerMainFunc(String(context->options.binDir), exeName); + if (func) + { + StringBuilder stdErrorString; + StringBuilder stdOutString; + + // Say static so not released + StringWriter stdError(&stdErrorString, WriterFlag::IsConsole | WriterFlag::IsStatic); + StringWriter stdOut(&stdOutString, WriterFlag::IsConsole | WriterFlag::IsStatic); + + StdWriters* prevStdWriters = StdWriters::getSingleton(); + + StdWriters stdWriters; + stdWriters.setWriter(SLANG_WRITER_CHANNEL_STD_ERROR, &stdError); + stdWriters.setWriter(SLANG_WRITER_CHANNEL_STD_OUTPUT, &stdOut); + + if (exeName == "slangc") + { + stdWriters.setWriter(SLANG_WRITER_CHANNEL_DIAGNOSTIC, &stdError); + } + + List args; + args.Add(exeName.Buffer()); + for (int i = 0; i < int(spawner.argumentList_.Count()); ++i) + { + args.Add(spawner.argumentList_[i].Buffer()); + } + + SlangResult res = func(&stdWriters, context->getSession(), int(args.Count()), args.begin()); + + StdWriters::setSingleton(prevStdWriters); + + spawner.standardError_ = stdErrorString; + spawner.standardOutput_ = stdOutString; + + spawner.resultCode_ = TestToolUtil::getReturnCode(res); + + return kOSError_None; + } + } + + if (options.shouldBeVerbose) + { + String commandLine = spawner.getCommandLine(); + context->reporter->messageFormat(TestMessageType::Info, "%s\n", commandLine.begin()); + } + + + OSError err = spawner.spawnAndWaitForCompletion(); + if (err != kOSError_None) + { +// fprintf(stderr, "failed to run test '%S'\n", testPath.ToWString()); + context->reporter->messageFormat(TestMessageType::RunError, "failed to run test '%S'", testPath.ToWString().begin()); + } + return err; +} + +String getOutput(OSProcessSpawner& spawner) +{ + OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); + + String standardOuptut = spawner.getStandardOutput(); + String standardError = spawner.getStandardError(); + + // We construct a single output string that captures the results + StringBuilder actualOutputBuilder; + actualOutputBuilder.Append("result code = "); + actualOutputBuilder.Append(resultCode); + actualOutputBuilder.Append("\nstandard error = {\n"); + actualOutputBuilder.Append(standardError); + actualOutputBuilder.Append("}\nstandard output = {\n"); + actualOutputBuilder.Append(standardOuptut); + actualOutputBuilder.Append("}\n"); + + return actualOutputBuilder.ProduceString(); +} + +// Finds the specialized or default path for expected data for a test. +// If neither are found, will return an empty string +String findExpectedPath(const TestInput& input, const char* postFix) +{ + StringBuilder specializedBuf; + + // Try the specialized name first + specializedBuf << input.outputStem; + if (postFix) + { + specializedBuf << postFix; + } + if (File::Exists(specializedBuf)) + { + return specializedBuf; + } + + + // Try the default name + StringBuilder defaultBuf; + defaultBuf.Clear(); + defaultBuf << input.filePath; + if (postFix) + { + defaultBuf << postFix; + } + + if (File::Exists(defaultBuf)) + { + return defaultBuf; + } + + // Couldn't find either + printf("referenceOutput '%s' or '%s' not found.\n", defaultBuf.Buffer(), specializedBuf.Buffer()); + + return ""; +} + +TestResult runSimpleTest(TestContext* context, TestInput& input) +{ + // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect + + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushArgument(filePath999); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + String actualOutput = getOutput(spawner); + + String expectedOutputPath = outputStem + ".expected"; + String expectedOutput; + try + { + expectedOutput = Slang::File::ReadAllText(expectedOutputPath); + } + catch (Slang::IOException) + { + } + + // If no expected output file was found, then we + // expect everything to be empty + if (expectedOutput.Length() == 0) + { + expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; + } + + TestResult result = TestResult::Pass; + + // Otherwise we compare to the expected output + if (actualOutput != expectedOutput) + { + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + result = TestResult::Fail; + } + + // If the test failed, then we write the actual output to a file + // so that we can easily diff it from the command line and + // diagnose the problem. + if (result == TestResult::Fail) + { + String actualOutputPath = outputStem + ".actual"; + Slang::File::WriteAllText(actualOutputPath, actualOutput); + + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + } + + return result; +} + +TestResult runReflectionTest(TestContext* context, TestInput& input) +{ + const auto& options = context->options; + auto filePath = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(options.binDir) + "slang-reflection-test" + osGetExecutableSuffix()); + spawner.pushArgument(filePath); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + String actualOutput = getOutput(spawner); + + String expectedOutputPath = outputStem + ".expected"; + String expectedOutput; + try + { + expectedOutput = Slang::File::ReadAllText(expectedOutputPath); + } + catch (Slang::IOException) + { + } + + // If no expected output file was found, then we + // expect everything to be empty + if (expectedOutput.Length() == 0) + { + expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; + } + + TestResult result = TestResult::Pass; + + // Otherwise we compare to the expected output + if (actualOutput != expectedOutput) + { + result = TestResult::Fail; + } + + // If the test failed, then we write the actual output to a file + // so that we can easily diff it from the command line and + // diagnose the problem. + if (result == TestResult::Fail) + { + String actualOutputPath = outputStem + ".actual"; + Slang::File::WriteAllText(actualOutputPath, actualOutput); + + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + } + + return result; +} + +String getExpectedOutput(String const& outputStem) +{ + String expectedOutputPath = outputStem + ".expected"; + String expectedOutput; + try + { + expectedOutput = Slang::File::ReadAllText(expectedOutputPath); + } + catch (Slang::IOException) + { + } + + // If no expected output file was found, then we + // expect everything to be empty + if (expectedOutput.Length() == 0) + { + expectedOutput = "result code = 0\nstandard error = {\n}\nstandard output = {\n}\n"; + } + + return expectedOutput; +} + +static SlangCompileTarget _getCompileTarget(const UnownedStringSlice& name) +{ +#define CASE(NAME, TARGET) if(name == NAME) return SLANG_##TARGET; + + CASE("hlsl", HLSL) + CASE("glsl", GLSL) + CASE("dxbc", DXBC) + CASE("dxbc-assembly", DXBC_ASM) + CASE("dxbc-asm", DXBC_ASM) + CASE("spirv", SPIRV) + CASE("spirv-assembly", SPIRV_ASM) + CASE("spirv-asm", SPIRV_ASM) + CASE("dxil", DXIL) + CASE("dxil-assembly", DXIL_ASM) + CASE("dxil-asm", DXIL_ASM) +#undef CASE + + return SLANG_TARGET_UNKNOWN; +} + +TestResult runCrossCompilerTest(TestContext* context, TestInput& input) +{ + // need to execute the stand-alone Slang compiler on the file + // then on the same file + `.glsl` and compare output + + auto filePath = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner actualSpawner; + OSProcessSpawner expectedSpawner; + + actualSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + expectedSpawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + + actualSpawner.pushArgument(filePath); + + const auto& args = input.testOptions->args; + + const UInt targetIndex = args.IndexOf("-target"); + if (targetIndex != UInt(-1) && targetIndex + 1 < args.Count()) + { + SlangCompileTarget target = _getCompileTarget(args[targetIndex + 1].getUnownedSlice()); + + // Check the session supports it. If not we ignore it + if (SLANG_FAILED(spSessionCheckCompileTargetSupport(context->getSession(), target))) + { + return TestResult::Ignored; + } + + switch (target) + { + case SLANG_DXIL_ASM: + { + expectedSpawner.pushArgument(filePath + ".hlsl"); + expectedSpawner.pushArgument("-pass-through"); + expectedSpawner.pushArgument("dxc"); + break; + } + default: + { + expectedSpawner.pushArgument(filePath + ".glsl"); + expectedSpawner.pushArgument("-pass-through"); + expectedSpawner.pushArgument("glslang"); + break; + } + } + } + + for( auto arg : input.testOptions->args ) + { + actualSpawner.pushArgument(arg); + expectedSpawner.pushArgument(arg); + } + + if (spawnAndWait(context, outputStem, expectedSpawner) != kOSError_None) + { + return TestResult::Fail; + } + + String expectedOutput = getOutput(expectedSpawner); + String expectedOutputPath = outputStem + ".expected"; + try + { + Slang::File::WriteAllText(expectedOutputPath, expectedOutput); + } + catch (Slang::IOException) + { + return TestResult::Fail; + } + + if (spawnAndWait(context, outputStem, actualSpawner) != kOSError_None) + { + return TestResult::Fail; + } + String actualOutput = getOutput(actualSpawner); + + TestResult result = TestResult::Pass; + + // Otherwise we compare to the expected output + if (actualOutput != expectedOutput) + { + result = TestResult::Fail; + } + + // Always fail if the compilation produced a failure, just + // to catch situations where, e.g., command-line options parsing + // caused the same error in both the Slang and glslang cases. + // + if( actualSpawner.getResultCode() != 0 ) + { + result = TestResult::Fail; + } + + // If the test failed, then we write the actual output to a file + // so that we can easily diff it from the command line and + // diagnose the problem. + if (result == TestResult::Fail) + { + String actualOutputPath = outputStem + ".actual"; + Slang::File::WriteAllText(actualOutputPath, actualOutput); + + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + } + + return result; +} + + +#ifdef SLANG_TEST_SUPPORT_HLSL +TestResult generateHLSLBaseline(TestContext* context, TestInput& input) +{ + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner spawner; + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushArgument(filePath999); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + spawner.pushArgument("-target"); + spawner.pushArgument("dxbc-assembly"); + spawner.pushArgument("-pass-through"); + spawner.pushArgument("fxc"); + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + String expectedOutput = getOutput(spawner); + String expectedOutputPath = outputStem + ".expected"; + try + { + Slang::File::WriteAllText(expectedOutputPath, expectedOutput); + } + catch (Slang::IOException) + { + return TestResult::Fail; + } + return TestResult::Pass; +} + +TestResult runHLSLComparisonTest(TestContext* context, TestInput& input) +{ + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + // We will use the Microsoft compiler to generate out expected output here + String expectedOutputPath = outputStem + ".expected"; + + // Generate the expected output using standard HLSL compiler + generateHLSLBaseline(context, input); + + // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushArgument(filePath999); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + // TODO: The compiler should probably define this automatically... + spawner.pushArgument("-D"); + spawner.pushArgument("__SLANG__"); + + spawner.pushArgument("-target"); + spawner.pushArgument("dxbc-assembly"); + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + // We ignore output to stdout, and only worry about what the compiler + // wrote to stderr. + + OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); + + String standardOutput = spawner.getStandardOutput(); + String standardError = spawner.getStandardError(); + + // We construct a single output string that captures the results + StringBuilder actualOutputBuilder; + actualOutputBuilder.Append("result code = "); + actualOutputBuilder.Append(resultCode); + actualOutputBuilder.Append("\nstandard error = {\n"); + actualOutputBuilder.Append(standardError); + actualOutputBuilder.Append("}\nstandard output = {\n"); + actualOutputBuilder.Append(standardOutput); + actualOutputBuilder.Append("}\n"); + + String actualOutput = actualOutputBuilder.ProduceString(); + + String expectedOutput; + try + { + expectedOutput = Slang::File::ReadAllText(expectedOutputPath); + } + catch (Slang::IOException) + { + } + + TestResult result = TestResult::Pass; + + // If no expected output file was found, then we + // expect everything to be empty + if (expectedOutput.Length() == 0) + { + if (resultCode != 0) result = TestResult::Fail; + if (standardError.Length() != 0) result = TestResult::Fail; + if (standardOutput.Length() != 0) result = TestResult::Fail; + } + // Otherwise we compare to the expected output + else if (actualOutput != expectedOutput) + { + result = TestResult::Fail; + } + + // Always fail if the compilation produced a failure, just + // to catch situations where, e.g., command-line options parsing + // caused the same error in both the Slang and fxc cases. + // + if( resultCode != 0 ) + { + result = TestResult::Fail; + } + + // If the test failed, then we write the actual output to a file + // so that we can easily diff it from the command line and + // diagnose the problem. + if (result == TestResult::Fail) + { + String actualOutputPath = outputStem + ".actual"; + Slang::File::WriteAllText(actualOutputPath, actualOutput); + + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + } + + return result; +} +#endif + +TestResult doGLSLComparisonTestRun(TestContext* context, + TestInput& input, + char const* langDefine, + char const* passThrough, + char const* outputKind, + String* outOutput) +{ + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(context->options.binDir) + "slangc" + osGetExecutableSuffix()); + spawner.pushArgument(filePath999); + + if( langDefine ) + { + spawner.pushArgument("-D"); + spawner.pushArgument(langDefine); + } + + if( passThrough ) + { + spawner.pushArgument("-pass-through"); + spawner.pushArgument(passThrough); + } + + spawner.pushArgument("-target"); + spawner.pushArgument("spirv-assembly"); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); + + String standardOuptut = spawner.getStandardOutput(); + String standardError = spawner.getStandardError(); + + // We construct a single output string that captures the results + StringBuilder outputBuilder; + outputBuilder.Append("result code = "); + outputBuilder.Append(resultCode); + outputBuilder.Append("\nstandard error = {\n"); + outputBuilder.Append(standardError); + outputBuilder.Append("}\nstandard output = {\n"); + outputBuilder.Append(standardOuptut); + outputBuilder.Append("}\n"); + + String outputPath = outputStem + outputKind; + String output = outputBuilder.ProduceString(); + + *outOutput = output; + + return TestResult::Pass; +} + +TestResult runGLSLComparisonTest(TestContext* context, TestInput& input) +{ + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + String expectedOutput; + String actualOutput; + + TestResult hlslResult = doGLSLComparisonTestRun(context, input, "__GLSL__", "glslang", ".expected", &expectedOutput); + TestResult slangResult = doGLSLComparisonTestRun(context, input, "__SLANG__", nullptr, ".actual", &actualOutput); + + Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); + Slang::File::WriteAllText(outputStem + ".actual", actualOutput); + + if( hlslResult == TestResult::Fail ) return TestResult::Fail; + if( slangResult == TestResult::Fail ) return TestResult::Fail; + + if (actualOutput != expectedOutput) + { + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + + return TestResult::Fail; + } + + return TestResult::Pass; +} + + +TestResult runComputeComparisonImpl(TestContext* context, TestInput& input, const char *const* langOpts, size_t numLangOpts) +{ + // TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a false pass + auto filePath999 = input.filePath; + auto outputStem = input.outputStem; + + const String referenceOutput = findExpectedPath(input, ".expected.txt"); + if (referenceOutput.Length() <= 0) + { + return TestResult::Fail; + } + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); + spawner.pushArgument(filePath999); + + for (auto arg : input.testOptions->args) + { + spawner.pushArgument(arg); + } + + for (int i = 0; i < int(numLangOpts); ++i) + { + spawner.pushArgument(langOpts[i]); + } + spawner.pushArgument("-o"); + auto actualOutputFile = outputStem + ".actual.txt"; + spawner.pushArgument(actualOutputFile); + + // clear the stale actual output file first. This will allow us to detect error if render-test fails and outputs nothing. + File::WriteAllText(actualOutputFile, ""); + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + printf("error spawning render-test\n"); + return TestResult::Fail; + } + + auto actualOutput = getOutput(spawner); + auto expectedOutput = getExpectedOutput(outputStem); + if (actualOutput != expectedOutput) + { + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + + String actualOutputPath = outputStem + ".actual"; + Slang::File::WriteAllText(actualOutputPath, actualOutput); + + return TestResult::Fail; + } + + // check against reference output + if (!File::Exists(actualOutputFile)) + { + printf("render-test not producing expected outputs.\n"); + printf("render-test output:\n%s\n", actualOutput.Buffer()); + return TestResult::Fail; + } + if (!File::Exists(referenceOutput)) + { + printf("referenceOutput %s not found.\n", referenceOutput.Buffer()); + return TestResult::Fail; + } + auto actualOutputContent = File::ReadAllText(actualOutputFile); + auto actualProgramOutput = Split(actualOutputContent, '\n'); + auto referenceProgramOutput = Split(File::ReadAllText(referenceOutput), '\n'); + auto printOutput = [&]() + { + context->reporter->messageFormat(TestMessageType::TestFailure, "output mismatch! actual output: {\n%s\n}, \n%s\n", actualOutputContent.Buffer(), actualOutput.Buffer()); + }; + if (actualProgramOutput.Count() < referenceProgramOutput.Count()) + { + printOutput(); + return TestResult::Fail; + } + for (int i = 0; i < (int)referenceProgramOutput.Count(); i++) + { + auto reference = String(referenceProgramOutput[i].Trim()); + auto actual = String(actualProgramOutput[i].Trim()); + if (actual != reference) + { + printOutput(); + return TestResult::Fail; + } + } + return TestResult::Pass; +} + +TestResult runSlangComputeComparisonTest(TestContext* context, TestInput& input) +{ + const char* langOpts[] = { "-slang", "-compute" }; + return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); +} + +TestResult runSlangComputeComparisonTestEx(TestContext* context, TestInput& input) +{ + return runComputeComparisonImpl(context, input, nullptr, 0); +} + +TestResult runHLSLComputeTest(TestContext* context, TestInput& input) +{ + const char* langOpts[] = { "--hlsl-rewrite", "-compute" }; + return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); +} + +TestResult runSlangRenderComputeComparisonTest(TestContext* context, TestInput& input) +{ + const char* langOpts[] = { "-slang", "-gcompute" }; + return runComputeComparisonImpl(context, input, langOpts, SLANG_COUNT_OF(langOpts)); +} + +TestResult doRenderComparisonTestRun(TestContext* context, TestInput& input, char const* langOption, char const* outputKind, String* outOutput) +{ + // TODO: delete any existing files at the output path(s) to avoid stale outputs leading to a false pass + + auto filePath = input.filePath; + auto outputStem = input.outputStem; + + OSProcessSpawner spawner; + + spawner.pushExecutablePath(String(context->options.binDir) + "render-test" + osGetExecutableSuffix()); + spawner.pushArgument(filePath); + + for( auto arg : input.testOptions->args ) + { + spawner.pushArgument(arg); + } + + spawner.pushArgument(langOption); + spawner.pushArgument("-o"); + spawner.pushArgument(outputStem + outputKind + ".png"); + + if (spawnAndWait(context, outputStem, spawner) != kOSError_None) + { + return TestResult::Fail; + } + + OSProcessSpawner::ResultCode resultCode = spawner.getResultCode(); + + String standardOutput = spawner.getStandardOutput(); + String standardError = spawner.getStandardError(); + + // We construct a single output string that captures the results + StringBuilder outputBuilder; + outputBuilder.Append("result code = "); + outputBuilder.Append(resultCode); + outputBuilder.Append("\nstandard error = {\n"); + outputBuilder.Append(standardError); + outputBuilder.Append("}\nstandard output = {\n"); + outputBuilder.Append(standardOutput); + outputBuilder.Append("}\n"); + + String outputPath = outputStem + outputKind; + String output = outputBuilder.ProduceString(); + + *outOutput = output; + + return TestResult::Pass; +} + +class STBImage +{ +public: + typedef STBImage ThisType; + + /// Reset back to default initialized state (frees any image set) + void reset(); + /// True if rhs has same size and amount of channels + bool isComparable(const ThisType& rhs) const; + + /// The width in pixels + int getWidth() const { return m_width; } + /// The height in pixels + int getHeight() const { return m_height; } + /// The number of channels (typically held as bytes in order) + int getNumChannels() const { return m_numChannels; } + + /// Get the contained pixels, nullptr if nothing loaded + const unsigned char* getPixels() const { return m_pixels; } + unsigned char* getPixels() { return m_pixels; } + + /// Read an image with filename. SLANG_OK on success + SlangResult read(const char* filename); + + ~STBImage() { reset(); } + + int m_width = 0; + int m_height = 0; + int m_numChannels = 0; + unsigned char* m_pixels = nullptr; +}; + +void STBImage::reset() +{ + if (m_pixels) + { + stbi_image_free(m_pixels); + m_pixels = nullptr; + } + m_width = 0; + m_height = 0; + m_numChannels = 0; +} + +SlangResult STBImage::read(const char* filename) +{ + reset(); + + m_pixels = stbi_load(filename, &m_width, &m_height, &m_numChannels, 0); + if (!m_pixels) + { + return SLANG_FAIL; + } + return SLANG_OK; +} + +bool STBImage::isComparable(const ThisType& rhs) const +{ + return (this == &rhs) || + (m_width == rhs.m_width && m_height == rhs.m_height && m_numChannels == rhs.m_numChannels); +} + + +TestResult doImageComparison(TestContext* context, String const& filePath) +{ + auto reporter = context->reporter; + + // Allow a difference in the low bits of the 8-bit result, just to play it safe + static const int kAbsoluteDiffCutoff = 2; + + // Allow a relative 1% difference + static const float kRelativeDiffCutoff = 0.01f; + + String expectedPath = filePath + ".expected.png"; + String actualPath = filePath + ".actual.png"; + + STBImage expectedImage; + if (SLANG_FAILED(expectedImage.read(expectedPath.Buffer()))) + { + reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", expectedPath.Buffer()); + return TestResult::Fail; + } + + STBImage actualImage; + if (SLANG_FAILED(actualImage.read(actualPath.Buffer()))) + { + reporter->messageFormat(TestMessageType::RunError, "Unable to load image ;%s'", actualPath.Buffer()); + return TestResult::Fail; + } + + if (!expectedImage.isComparable(actualImage)) + { + reporter->messageFormat(TestMessageType::TestFailure, "Images are different sizes '%s' '%s'", actualPath.Buffer(), expectedPath.Buffer()); + return TestResult::Fail; + } + + { + const unsigned char* expectedPixels = expectedImage.getPixels(); + const unsigned char* actualPixels = actualImage.getPixels(); + + const int height = actualImage.getHeight(); + const int width = actualImage.getWidth(); + const int numChannels = actualImage.getNumChannels(); + const int rowSize = width * numChannels; + + for (int y = 0; y < height; ++y) + { + for (int i = 0; i < rowSize; ++i) + { + int expectedVal = expectedPixels[i]; + int actualVal = actualPixels[i]; + + int absoluteDiff = actualVal - expectedVal; + if (absoluteDiff < 0) absoluteDiff = -absoluteDiff; + + if (absoluteDiff < kAbsoluteDiffCutoff) + { + // There might be a difference, but we'll consider it to be inside tolerance + continue; + } + + float relativeDiff = 0.0f; + if (expectedVal != 0) + { + relativeDiff = fabsf(float(actualVal) - float(expectedVal)) / float(expectedVal); + + if (relativeDiff < kRelativeDiffCutoff) + { + // relative difference was small enough + continue; + } + } + + // TODO: may need to do some local search sorts of things, to deal with + // cases where vertex shader results lead to rendering that is off + // by one pixel... + + const int x = i / numChannels; + const int channelIndex = i % numChannels; + + reporter->messageFormat(TestMessageType::TestFailure, "image compare failure at (%d,%d) channel %d. expected %d got %d (absolute error: %d, relative error: %f)\n", + x, y, channelIndex, + expectedVal, + actualVal, + absoluteDiff, + relativeDiff); + + // There was a difference we couldn't excuse! + return TestResult::Fail; + } + + expectedPixels += rowSize; + actualPixels += rowSize; + } + } + + return TestResult::Pass; +} + +TestResult runHLSLRenderComparisonTestImpl( + TestContext* context, + TestInput& input, + char const* expectedArg, + char const* actualArg) +{ + auto filePath = input.filePath; + auto outputStem = input.outputStem; + + String expectedOutput; + String actualOutput; + + TestResult hlslResult = doRenderComparisonTestRun(context, input, expectedArg, ".expected", &expectedOutput); + TestResult slangResult = doRenderComparisonTestRun(context, input, actualArg, ".actual", &actualOutput); + + Slang::File::WriteAllText(outputStem + ".expected", expectedOutput); + Slang::File::WriteAllText(outputStem + ".actual", actualOutput); + + if( hlslResult == TestResult::Fail ) return TestResult::Fail; + if( slangResult == TestResult::Fail ) return TestResult::Fail; + + if (actualOutput != expectedOutput) + { + context->reporter->dumpOutputDifference(expectedOutput, actualOutput); + + return TestResult::Fail; + } + + // Next do an image comparison on the expected output images! + + TestResult imageCompareResult = doImageComparison(context, outputStem); + if(imageCompareResult != TestResult::Pass) + return imageCompareResult; + + return TestResult::Pass; +} + +TestResult runHLSLRenderComparisonTest(TestContext* context, TestInput& input) +{ + return runHLSLRenderComparisonTestImpl(context, input, "-hlsl", "-slang"); +} + +TestResult runHLSLCrossCompileRenderComparisonTest(TestContext* context, TestInput& input) +{ + return runHLSLRenderComparisonTestImpl(context, input, "-slang", "-glsl-cross"); +} + +TestResult runHLSLAndGLSLRenderComparisonTest(TestContext* context, TestInput& input) +{ + return runHLSLRenderComparisonTestImpl(context, input, "-hlsl-rewrite", "-glsl-rewrite"); +} + +TestResult skipTest(TestContext* /* context */, TestInput& /*input*/) +{ + return TestResult::Ignored; +} + +static bool hasRenderOption(RenderApiType apiType, const List& options) +{ + const RenderApiUtil::Info& info = RenderApiUtil::getInfo(apiType); + + List namesList; + + for (UInt i = 0; i < options.Count(); ++i) + { + const String& option = options[i]; + + if (option.StartsWith("-")) + { + const UnownedStringSlice parameter(option.Buffer() + 1, option.Buffer() + option.Length()); + // See if we have a match + for (int j = 0; j < SLANG_COUNT_OF(RenderApiUtil::s_infos); j++) + { + const auto& apiInfo = RenderApiUtil::s_infos[j]; + const UnownedStringSlice names(info.names); + + if (names.indexOf(',') >= 0) + { + StringUtil::split(names, ',', namesList); + + if (namesList.IndexOf(parameter) != UInt(-1)) + { + return true; + } + } + else if (names == parameter) + { + return true; + } + } + } + } + + return false; +} + +bool hasRenderOption(RenderApiType apiType, const TestOptions& options) +{ + return hasRenderOption(apiType, options.args); +} + +bool hasRenderOption(RenderApiType apiType, const FileTestList& testList) +{ + const int numTests = int(testList.tests.Count()); + for (int i = 0; i < numTests; i++) + { + if (hasRenderOption(apiType, testList.tests[i].args)) + { + return true; + } + } + return false; +} + +bool isHLSLTest(const String& command) +{ + return command == "COMPARE_HLSL" || + command == "COMPARE_HLSL_RENDER" || + command == "COMPARE_HLSL_CROSS_COMPILE_RENDER" || + command == "COMPARE_HLSL_GLSL_RENDER"; +} + +bool isRenderTest(const String& command) +{ + return command == "COMPARE_COMPUTE" || + command == "COMPARE_COMPUTE_EX" || + command == "HLSL_COMPUTE" || + command == "COMPARE_RENDER_COMPUTE" || + command == "COMPARE_HLSL_RENDER" || + command == "COMPARE_HLSL_CROSS_COMPILE_RENDER" || + command == "COMPARE_HLSL_GLSL_RENDER"; +} + +static bool canIgnoreTestWithDisabledRenderer(const TestOptions& testOptions, const Options& appOptions) +{ + for (int i = 0; i < int(RenderApiType::CountOf); ++i) + { + RenderApiType apiType = RenderApiType(i); + RenderApiFlag::Enum apiFlag = RenderApiFlag::Enum(1 << i); + + if (hasRenderOption(apiType, testOptions) && (appOptions.enabledApis & apiFlag) == 0) + { + return true; + } + } + + return false; +} + +TestResult runTest( + TestContext* context, + String const& filePath, + String const& outputStem, + TestOptions const& testOptions, + FileTestList const& testList) +{ + // If this test can be ignored + if (canIgnoreTestWithDisabledRenderer(testOptions, context->options)) + { + return TestResult::Ignored; + } + + // based on command name, dispatch to an appropriate callback + struct TestCommands + { + char const* name; + TestCallback callback; + }; + + static const TestCommands kTestCommands[] = + { + { "SIMPLE", &runSimpleTest}, + { "REFLECTION", &runReflectionTest}, +#if SLANG_TEST_SUPPORT_HLSL + { "COMPARE_HLSL", &runHLSLComparisonTest}, + { "COMPARE_HLSL_RENDER", &runHLSLRenderComparisonTest}, + { "COMPARE_HLSL_CROSS_COMPILE_RENDER", &runHLSLCrossCompileRenderComparisonTest}, + { "COMPARE_HLSL_GLSL_RENDER", &runHLSLAndGLSLRenderComparisonTest }, + { "COMPARE_COMPUTE", runSlangComputeComparisonTest}, + { "COMPARE_COMPUTE_EX", runSlangComputeComparisonTestEx}, + { "HLSL_COMPUTE", runHLSLComputeTest}, + { "COMPARE_RENDER_COMPUTE", &runSlangRenderComputeComparisonTest }, + +#else + { "COMPARE_HLSL", &skipTest }, + { "COMPARE_HLSL_RENDER", &skipTest }, + { "COMPARE_HLSL_CROSS_COMPILE_RENDER", &skipTest}, + { "COMPARE_HLSL_GLSL_RENDER", &skipTest }, + { "COMPARE_COMPUTE", &skipTest}, + { "COMPARE_COMPUTE_EX", &skipTest}, + { "HLSL_COMPUTE", &skipTest}, + { "COMPARE_RENDER_COMPUTE", &skipTest }, +#endif + { "COMPARE_GLSL", &runGLSLComparisonTest }, + { "CROSS_COMPILE", &runCrossCompilerTest }, + { nullptr, nullptr }, + }; + + for( auto ii = kTestCommands; ii->name; ++ii ) + { + if(testOptions.command != ii->name) + continue; + + TestInput testInput; + testInput.filePath = filePath; + testInput.outputStem = outputStem; + testInput.testOptions = &testOptions; + testInput.testList = &testList; + + { + TestReporter* reporter = context->reporter; + TestReporter::TestScope scope(reporter, outputStem); + + TestResult testResult = ii->callback(context, testInput); + reporter->addResult(testResult); + + return testResult; + } + } + + // No actual test runner found! + + return TestResult::Fail; +} + +bool testCategoryMatches( + TestCategory* sub, + TestCategory* sup) +{ + auto ss = sub; + while(ss) + { + if(ss == sup) + return true; + + ss = ss->parent; + } + return false; +} + +bool testCategoryMatches( + TestCategory* categoryToMatch, + Dictionary categorySet) +{ + for( auto item : categorySet ) + { + if(testCategoryMatches(categoryToMatch, item.Value)) + return true; + } + return false; +} + +bool testPassesCategoryMask( + TestContext* context, + TestOptions const& test) +{ + // Don't include a test we should filter out + for( auto testCategory : test.categories ) + { + if(testCategoryMatches(testCategory, context->options.excludeCategories)) + return false; + } + + // Otherwise inclue any test the user asked for + for( auto testCategory : test.categories ) + { + if(testCategoryMatches(testCategory, context->options.includeCategories)) + return true; + } + + // skip by default + return false; +} + +void runTestsOnFile( + TestContext* context, + String filePath) +{ + // Gather a list of tests to run + FileTestList testList; + + if( gatherTestsForFile(&context->categorySet, filePath, &testList) == TestResult::Ignored ) + { + // Test was explicitly ignored + return; + } + + // Note cases where a test file exists, but we found nothing to run + if( testList.tests.Count() == 0 ) + { + context->reporter->addTest(filePath, TestResult::Ignored); + return; + } + + List synthesizedTests; + + // If dx12 is available synthesize Dx12 test + if ((context->options.synthesizedTestApis & RenderApiFlag::D3D12) != 0) + { + // If doesn't have option generate dx12 options from dx11 + if (!hasRenderOption(RenderApiType::D3D12, testList)) + { + const int numTests = int(testList.tests.Count()); + for (int i = 0; i < numTests; i++) + { + const TestOptions& testOptions = testList.tests[i]; + // If it's a render test, and there is on d3d option, add one + if (isRenderTest(testOptions.command) && !hasRenderOption(RenderApiType::D3D12, testOptions)) + { + // Add with -dx12 option + TestOptions testOptionsCopy(testOptions); + testOptionsCopy.args.Add("-dx12"); + + synthesizedTests.Add(testOptionsCopy); + } + } + } + } + + // If Vulkan is available synthesize Vulkan test + if ((context->options.synthesizedTestApis & RenderApiFlag::Vulkan) != 0) + { + // If doesn't have option generate dx12 options from dx11 + if (!hasRenderOption(RenderApiType::Vulkan, testList)) + { + const int numTests = int(testList.tests.Count()); + for (int i = 0; i < numTests; i++) + { + const TestOptions& testOptions = testList.tests[i]; + // If it's a render test, and there is on d3d option, add one + if (isRenderTest(testOptions.command) && !isHLSLTest(testOptions.command) && !hasRenderOption(RenderApiType::Vulkan, testOptions)) + { + // Add with -vk option + TestOptions testOptionsCopy(testOptions); + testOptionsCopy.args.Add("-vk"); + + UInt index = testOptionsCopy.args.IndexOf("-hlsl"); + if (index != UInt(-1)) + { + testOptionsCopy.args.RemoveAt(index); + } + + synthesizedTests.Add(testOptionsCopy); + } + } + } + } + + // Add any tests that were synthesized + for (UInt i = 0; i < synthesizedTests.Count(); ++i) + { + testList.tests.Add(synthesizedTests[i]); + } + + // We have found a test to run! + int subTestCount = 0; + for( auto& tt : testList.tests ) + { + int subTestIndex = subTestCount++; + + // Check that the test passes our current category mask + if(!testPassesCategoryMask(context, tt)) + { + continue; + } + + String outputStem = filePath; + if(subTestIndex != 0) + { + outputStem = outputStem + "." + String(subTestIndex); + } + + /* TestResult result = */ runTest(context, filePath, outputStem, tt, testList); + + // Could determine if to continue or not here... based on result + } +} + + +static bool endsWithAllowedExtension( + TestContext* /*context*/, + String filePath) +{ + char const* allowedExtensions[] = { + ".slang", + ".hlsl", + ".fx", + ".glsl", + ".vert", + ".frag", + ".geom", + ".tesc", + ".tese", + ".comp", + ".internal", + ".ahit", + ".chit", + ".miss", + ".rgen", + nullptr }; + + for( auto ii = allowedExtensions; *ii; ++ii ) + { + if(filePath.EndsWith(*ii)) + return true; + } + + return false; +} + +static bool shouldRunTest( + TestContext* context, + String filePath) +{ + if(!endsWithAllowedExtension(context, filePath)) + return false; + + if( context->options.testPrefix ) + { + if( strncmp(context->options.testPrefix, filePath.begin(), strlen(context->options.testPrefix)) != 0 ) + { + return false; + } + } + + return true; +} + +void runTestsInDirectory( + TestContext* context, + String directoryPath) +{ + for (auto file : osFindFilesInDirectory(directoryPath)) + { + if( shouldRunTest(context, file) ) + { +// fprintf(stderr, "slang-test: found '%s'\n", file.Buffer()); + runTestsOnFile(context, file); + } + } + for (auto subdir : osFindChildDirectories(directoryPath)) + { + runTestsInDirectory(context, subdir); + } +} + +SlangResult innerMain(int argc, char** argv) +{ + StdWriters::initDefault(); + + // The context holds useful things used during testing + TestContext context; + SLANG_RETURN_ON_FAIL(SLANG_FAILED(context.init())) + + auto& categorySet = context.categorySet; + + // Set up our test categories here + auto fullTestCategory = categorySet.add("full", nullptr); + auto quickTestCategory = categorySet.add("quick", fullTestCategory); + /*auto smokeTestCategory = */categorySet.add("smoke", quickTestCategory); + auto renderTestCategory = categorySet.add("render", fullTestCategory); + /*auto computeTestCategory = */categorySet.add("compute", fullTestCategory); + auto vulkanTestCategory = categorySet.add("vulkan", fullTestCategory); + auto unitTestCatagory = categorySet.add("unit-test", fullTestCategory); + auto compatibilityIssueCatagory = categorySet.add("compatibility-issue", fullTestCategory); + + // An un-categorized test will always belong to the `full` category + categorySet.defaultCategory = fullTestCategory; + + { + // We can set the slangc command line tool, to just use the function defined here + context.setInnerMainFunc("slangc", &SlangCTool::innerMain); + } + + SLANG_RETURN_ON_FAIL(Options::parse(argc, argv, &categorySet, StdWriters::getError(), &context.options)); + + Options& options = context.options; + + if (options.subCommand.Length()) + { + // Get the function from the tool + auto func = context.getInnerMainFunc(options.binDir, options.subCommand); + if (!func) + { + StdWriters::getError().print("error: Unable to launch tool '%s'\n", options.subCommand.Buffer()); + return SLANG_FAIL; + } + + // Copy args to a char* list + const auto& srcArgs = options.subCommandArgs; + List args; + args.SetSize(srcArgs.Count()); + for (UInt i = 0; i < srcArgs.Count(); ++i) + { + args[i] = srcArgs[i].Buffer(); + } + + return func(StdWriters::getSingleton(), context.getSession(), int(args.Count()), args.Buffer()); + } + + if( options.includeCategories.Count() == 0 ) + { + options.includeCategories.Add(fullTestCategory, fullTestCategory); + } + + // Exclude rendering tests when building under AppVeyor. + // + // TODO: this is very ad hoc, and we should do something cleaner. + if( options.outputMode == TestOutputMode::AppVeyor ) + { + options.excludeCategories.Add(renderTestCategory, renderTestCategory); + options.excludeCategories.Add(vulkanTestCategory, vulkanTestCategory); + } + + { + // Setup the reporter + TestReporter reporter; + SLANG_RETURN_ON_FAIL(reporter.init(options.outputMode)); + + context.reporter = &reporter; + + reporter.m_dumpOutputOnFailure = options.dumpOutputOnFailure; + reporter.m_isVerbose = options.shouldBeVerbose; + + { + TestReporter::SuiteScope suiteScope(&reporter, "tests"); + // Enumerate test files according to policy + // TODO: add more directories to this list + // TODO: allow for a command-line argument to select a particular directory + runTestsInDirectory(&context, "tests/"); + } + + // Run the unit tests (these are internal C++ tests - not specified via files in a directory) + // They are registered with SLANG_UNIT_TEST macro + { + TestReporter::SuiteScope suiteScope(&reporter, "unit tests"); + TestReporter::set(&reporter); + + // Run the unit tests + TestRegister* cur = TestRegister::s_first; + while (cur) + { + StringBuilder filePath; + filePath << "unit-tests/" << cur->m_name << ".internal"; + + TestOptions testOptions; + testOptions.categories.Add(unitTestCatagory); + testOptions.command = filePath; + + if (shouldRunTest(&context, testOptions.command)) + { + if (testPassesCategoryMask(&context, testOptions)) + { + reporter.startTest(testOptions.command); + // Run the test function + cur->m_func(); + reporter.endTest(); + } + else + { + reporter.addTest(testOptions.command, TestResult::Ignored); + } + } + + // Next + cur = cur->m_next; + } + + TestReporter::set(nullptr); + } + + reporter.outputSummary(); + return reporter.didAllSucceed() ? SLANG_OK : SLANG_FAIL; + } +} + +int main(int argc, char** argv) +{ + const SlangResult res = innerMain(argc, argv); +#ifdef _MSC_VER + _CrtDumpMemoryLeaks(); +#endif + return SLANG_SUCCEEDED(res) ? 0 : 1; +} diff --git a/tools/slang-test/slang-test.vcxproj b/tools/slang-test/slang-test.vcxproj index cbd00ad0e..520ee1759 100644 --- a/tools/slang-test/slang-test.vcxproj +++ b/tools/slang-test/slang-test.vcxproj @@ -170,10 +170,10 @@ - + diff --git a/tools/slang-test/slang-test.vcxproj.filters b/tools/slang-test/slang-test.vcxproj.filters index 6c1bdf941..be03d5273 100644 --- a/tools/slang-test/slang-test.vcxproj.filters +++ b/tools/slang-test/slang-test.vcxproj.filters @@ -29,9 +29,6 @@ - - Source Files - Source Files @@ -41,6 +38,9 @@ Source Files + + Source Files + Source Files -- cgit v1.2.3