blob: 3ce3267fddf207e64e9ddbfa3d863b6be2297f92 [file] [log] [blame]
/*
* Copyright 2014 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/gl/builders/GrGLProgramBuilder.h"
#include "include/gpu/GrContext.h"
#include "src/core/SkATrace.h"
#include "src/core/SkAutoMalloc.h"
#include "src/core/SkReader32.h"
#include "src/core/SkTraceEvent.h"
#include "src/core/SkWriter32.h"
#include "src/gpu/GrAutoLocaleSetter.h"
#include "src/gpu/GrContextPriv.h"
#include "src/gpu/GrCoordTransform.h"
#include "src/gpu/GrPersistentCacheUtils.h"
#include "src/gpu/GrProgramDesc.h"
#include "src/gpu/GrShaderCaps.h"
#include "src/gpu/GrShaderUtils.h"
#include "src/gpu/GrSwizzle.h"
#include "src/gpu/gl/GrGLGpu.h"
#include "src/gpu/gl/GrGLProgram.h"
#include "src/gpu/gl/builders/GrGLProgramBuilder.h"
#include "src/gpu/gl/builders/GrGLShaderStringBuilder.h"
#include "src/gpu/glsl/GrGLSLFragmentProcessor.h"
#include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
#include "src/gpu/glsl/GrGLSLProgramDataManager.h"
#include "src/gpu/glsl/GrGLSLXferProcessor.h"
#define GL_CALL(X) GR_GL_CALL(this->gpu()->glInterface(), X)
#define GL_CALL_RET(R, X) GR_GL_CALL_RET(this->gpu()->glInterface(), R, X)
static void cleanup_shaders(GrGLGpu* gpu, const SkTDArray<GrGLuint>& shaderIDs) {
for (int i = 0; i < shaderIDs.count(); ++i) {
GR_GL_CALL(gpu->glInterface(), DeleteShader(shaderIDs[i]));
}
}
static void cleanup_program(GrGLGpu* gpu, GrGLuint programID,
const SkTDArray<GrGLuint>& shaderIDs) {
GR_GL_CALL(gpu->glInterface(), DeleteProgram(programID));
cleanup_shaders(gpu, shaderIDs);
}
GrGLProgram* GrGLProgramBuilder::CreateProgram(GrRenderTarget* renderTarget,
const GrProgramInfo& programInfo,
GrProgramDesc* desc,
GrGLGpu* gpu,
const GrGLPrecompiledProgram* precompiledProgram) {
ATRACE_ANDROID_FRAMEWORK("Shader Compile");
GrAutoLocaleSetter als("C");
// create a builder. This will be handed off to effects so they can use it to add
// uniforms, varyings, textures, etc
GrGLProgramBuilder builder(gpu, renderTarget, programInfo, desc);
auto persistentCache = gpu->getContext()->priv().getPersistentCache();
if (persistentCache && !precompiledProgram) {
sk_sp<SkData> key = SkData::MakeWithoutCopy(desc->asKey(), desc->keyLength());
builder.fCached = persistentCache->load(*key);
// the eventual end goal is to completely skip emitAndInstallProcs on a cache hit, but it's
// doing necessary setup in addition to generating the SkSL code. Currently we are only able
// to skip the SkSL->GLSL step on a cache hit.
}
if (!builder.emitAndInstallProcs()) {
return nullptr;
}
return builder.finalize(precompiledProgram);
}
/////////////////////////////////////////////////////////////////////////////
GrGLProgramBuilder::GrGLProgramBuilder(GrGLGpu* gpu,
GrRenderTarget* renderTarget,
const GrProgramInfo& programInfo,
GrProgramDesc* desc)
: INHERITED(renderTarget, programInfo, desc)
, fGpu(gpu)
, fVaryingHandler(this)
, fUniformHandler(this)
, fVertexAttributeCnt(0)
, fInstanceAttributeCnt(0)
, fVertexStride(0)
, fInstanceStride(0) {}
const GrCaps* GrGLProgramBuilder::caps() const {
return fGpu->caps();
}
bool GrGLProgramBuilder::compileAndAttachShaders(const SkSL::String& glsl,
GrGLuint programId,
GrGLenum type,
SkTDArray<GrGLuint>* shaderIds,
GrContextOptions::ShaderErrorHandler* errHandler) {
GrGLGpu* gpu = this->gpu();
GrGLuint shaderId = GrGLCompileAndAttachShader(gpu->glContext(),
programId,
type,
glsl,
gpu->stats(),
errHandler);
if (!shaderId) {
return false;
}
*shaderIds->append() = shaderId;
return true;
}
void GrGLProgramBuilder::computeCountsAndStrides(GrGLuint programID,
const GrPrimitiveProcessor& primProc,
bool bindAttribLocations) {
fVertexAttributeCnt = primProc.numVertexAttributes();
fInstanceAttributeCnt = primProc.numInstanceAttributes();
fAttributes.reset(
new GrGLProgram::Attribute[fVertexAttributeCnt + fInstanceAttributeCnt]);
auto addAttr = [&](int i, const auto& a, size_t* stride) {
fAttributes[i].fCPUType = a.cpuType();
fAttributes[i].fGPUType = a.gpuType();
fAttributes[i].fOffset = *stride;
*stride += a.sizeAlign4();
fAttributes[i].fLocation = i;
if (bindAttribLocations) {
GL_CALL(BindAttribLocation(programID, i, a.name()));
}
};
fVertexStride = 0;
int i = 0;
for (const auto& attr : primProc.vertexAttributes()) {
addAttr(i++, attr, &fVertexStride);
}
SkASSERT(fVertexStride == primProc.vertexStride());
fInstanceStride = 0;
for (const auto& attr : primProc.instanceAttributes()) {
addAttr(i++, attr, &fInstanceStride);
}
SkASSERT(fInstanceStride == primProc.instanceStride());
}
void GrGLProgramBuilder::addInputVars(const SkSL::Program::Inputs& inputs) {
if (inputs.fRTWidth) {
this->addRTWidthUniform(SKSL_RTWIDTH_NAME);
}
if (inputs.fRTHeight) {
this->addRTHeightUniform(SKSL_RTHEIGHT_NAME);
}
}
static constexpr SkFourByteTag kSKSL_Tag = SkSetFourByteTag('S', 'K', 'S', 'L');
static constexpr SkFourByteTag kGLSL_Tag = SkSetFourByteTag('G', 'L', 'S', 'L');
static constexpr SkFourByteTag kGLPB_Tag = SkSetFourByteTag('G', 'L', 'P', 'B');
void GrGLProgramBuilder::storeShaderInCache(const SkSL::Program::Inputs& inputs, GrGLuint programID,
const SkSL::String shaders[], bool isSkSL,
SkSL::Program::Settings* settings) {
if (!this->gpu()->getContext()->priv().getPersistentCache()) {
return;
}
sk_sp<SkData> key = SkData::MakeWithoutCopy(this->desc()->asKey(), this->desc()->keyLength());
if (fGpu->glCaps().programBinarySupport()) {
// binary cache
GrGLsizei length = 0;
GL_CALL(GetProgramiv(programID, GL_PROGRAM_BINARY_LENGTH, &length));
if (length > 0) {
SkWriter32 writer;
writer.write32(kGLPB_Tag);
writer.writePad(&inputs, sizeof(inputs));
writer.write32(length);
void* binary = writer.reservePad(length);
GrGLenum binaryFormat;
GL_CALL(GetProgramBinary(programID, length, &length, &binaryFormat, binary));
writer.write32(binaryFormat);
auto data = writer.snapshotAsData();
this->gpu()->getContext()->priv().getPersistentCache()->store(*key, *data);
}
} else {
// source cache, plus metadata to allow for a complete precompile
GrPersistentCacheUtils::ShaderMetadata meta;
meta.fSettings = settings;
meta.fHasCustomColorOutput = fFS.hasCustomColorOutput();
meta.fHasSecondaryColorOutput = fFS.hasSecondaryOutput();
for (const auto& attr : this->primitiveProcessor().vertexAttributes()) {
meta.fAttributeNames.emplace_back(attr.name());
}
for (const auto& attr : this->primitiveProcessor().instanceAttributes()) {
meta.fAttributeNames.emplace_back(attr.name());
}
auto data = GrPersistentCacheUtils::PackCachedShaders(isSkSL ? kSKSL_Tag : kGLSL_Tag,
shaders, &inputs, 1, &meta);
this->gpu()->getContext()->priv().getPersistentCache()->store(*key, *data);
}
}
GrGLProgram* GrGLProgramBuilder::finalize(const GrGLPrecompiledProgram* precompiledProgram) {
TRACE_EVENT0("skia.gpu", TRACE_FUNC);
// verify we can get a program id
GrGLuint programID;
if (precompiledProgram) {
programID = precompiledProgram->fProgramID;
} else {
GL_CALL_RET(programID, CreateProgram());
}
if (0 == programID) {
return nullptr;
}
if (this->gpu()->glCaps().programBinarySupport() &&
this->gpu()->glCaps().programParameterSupport() &&
this->gpu()->getContext()->priv().getPersistentCache() &&
!precompiledProgram) {
GL_CALL(ProgramParameteri(programID, GR_GL_PROGRAM_BINARY_RETRIEVABLE_HINT, GR_GL_TRUE));
}
this->finalizeShaders();
// compile shaders and bind attributes / uniforms
auto errorHandler = this->gpu()->getContext()->priv().getShaderErrorHandler();
const GrPrimitiveProcessor& primProc = this->primitiveProcessor();
SkSL::Program::Settings settings;
settings.fCaps = this->gpu()->glCaps().shaderCaps();
settings.fFlipY = this->origin() != kTopLeft_GrSurfaceOrigin;
settings.fSharpenTextures =
this->gpu()->getContext()->priv().options().fSharpenMipmappedTextures;
settings.fFragColorIsInOut = this->fragColorIsInOut();
SkSL::Program::Inputs inputs;
SkTDArray<GrGLuint> shadersToDelete;
// Calling GetProgramiv is expensive in Chromium. Assume success in release builds.
bool checkLinked = kChromium_GrGLDriver != fGpu->ctxInfo().driver();
#ifdef SK_DEBUG
checkLinked = true;
#endif
bool cached = fCached.get() != nullptr;
bool usedProgramBinaries = false;
SkSL::String glsl[kGrShaderTypeCount];
SkSL::String* sksl[kGrShaderTypeCount] = {
&fVS.fCompilerString,
&fGS.fCompilerString,
&fFS.fCompilerString,
};
SkSL::String cached_sksl[kGrShaderTypeCount];
if (precompiledProgram) {
// This is very similar to when we get program binaries. We even set that flag, as it's
// used to prevent other compile work later, and to force re-querying uniform locations.
this->addInputVars(precompiledProgram->fInputs);
this->computeCountsAndStrides(programID, primProc, false);
usedProgramBinaries = true;
} else if (cached) {
SkReader32 reader(fCached->data(), fCached->size());
SkFourByteTag shaderType = reader.readU32();
switch (shaderType) {
case kGLPB_Tag: {
// Program binary cache hit. We may opt not to use this if we don't trust program
// binaries on this driver
if (!fGpu->glCaps().programBinarySupport()) {
cached = false;
break;
}
reader.read(&inputs, sizeof(inputs));
GrGLsizei length = reader.readInt();
const void* binary = reader.skip(length);
GrGLenum binaryFormat = reader.readU32();
GrGLClearErr(this->gpu()->glInterface());
GR_GL_CALL_NOERRCHECK(this->gpu()->glInterface(),
ProgramBinary(programID, binaryFormat,
const_cast<void*>(binary), length));
if (GR_GL_GET_ERROR(this->gpu()->glInterface()) == GR_GL_NO_ERROR) {
if (checkLinked) {
cached = this->checkLinkStatus(programID, errorHandler, nullptr, nullptr);
}
if (cached) {
this->addInputVars(inputs);
this->computeCountsAndStrides(programID, primProc, false);
}
} else {
cached = false;
}
usedProgramBinaries = cached;
break;
}
case kGLSL_Tag:
// Source cache hit, we don't need to compile the SkSL->GLSL
GrPersistentCacheUtils::UnpackCachedShaders(&reader, glsl, &inputs, 1);
break;
case kSKSL_Tag:
// SkSL cache hit, this should only happen in tools overriding the generated SkSL
GrPersistentCacheUtils::UnpackCachedShaders(&reader, cached_sksl, &inputs, 1);
for (int i = 0; i < kGrShaderTypeCount; ++i) {
sksl[i] = &cached_sksl[i];
}
break;
}
}
if (!usedProgramBinaries) {
// Either a cache miss, or we got something other than binaries from the cache
/*
Fragment Shader
*/
if (glsl[kFragment_GrShaderType].empty()) {
// Don't have cached GLSL, need to compile SkSL->GLSL
if (fFS.fForceHighPrecision) {
settings.fForceHighPrecision = true;
}
std::unique_ptr<SkSL::Program> fs = GrSkSLtoGLSL(gpu()->glContext(),
SkSL::Program::kFragment_Kind,
*sksl[kFragment_GrShaderType],
settings,
&glsl[kFragment_GrShaderType],
errorHandler);
if (!fs) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
inputs = fs->fInputs;
}
this->addInputVars(inputs);
if (!this->compileAndAttachShaders(glsl[kFragment_GrShaderType], programID,
GR_GL_FRAGMENT_SHADER, &shadersToDelete, errorHandler)) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
/*
Vertex Shader
*/
if (glsl[kVertex_GrShaderType].empty()) {
// Don't have cached GLSL, need to compile SkSL->GLSL
std::unique_ptr<SkSL::Program> vs = GrSkSLtoGLSL(gpu()->glContext(),
SkSL::Program::kVertex_Kind,
*sksl[kVertex_GrShaderType],
settings,
&glsl[kVertex_GrShaderType],
errorHandler);
if (!vs) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
}
if (!this->compileAndAttachShaders(glsl[kVertex_GrShaderType], programID,
GR_GL_VERTEX_SHADER, &shadersToDelete, errorHandler)) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
// This also binds vertex attribute locations. NVPR doesn't really use vertices,
// even though it requires a vertex shader in the program.
if (!primProc.isPathRendering()) {
this->computeCountsAndStrides(programID, primProc, true);
}
/*
Geometry Shader
*/
if (primProc.willUseGeoShader()) {
if (glsl[kGeometry_GrShaderType].empty()) {
// Don't have cached GLSL, need to compile SkSL->GLSL
std::unique_ptr<SkSL::Program> gs;
gs = GrSkSLtoGLSL(gpu()->glContext(),
SkSL::Program::kGeometry_Kind,
*sksl[kGeometry_GrShaderType],
settings,
&glsl[kGeometry_GrShaderType],
errorHandler);
if (!gs) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
}
if (!this->compileAndAttachShaders(glsl[kGeometry_GrShaderType], programID,
GR_GL_GEOMETRY_SHADER, &shadersToDelete,
errorHandler)) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
}
this->bindProgramResourceLocations(programID);
GL_CALL(LinkProgram(programID));
if (checkLinked) {
if (!this->checkLinkStatus(programID, errorHandler, sksl, glsl)) {
cleanup_program(fGpu, programID, shadersToDelete);
return nullptr;
}
}
}
this->resolveProgramResourceLocations(programID, usedProgramBinaries);
cleanup_shaders(fGpu, shadersToDelete);
// With ANGLE, we can't cache path-rendering programs. We use ProgramPathFragmentInputGen,
// and ANGLE's deserialized program state doesn't restore enough state to handle that.
// The native NVIDIA drivers do, but this is such an edge case that it's easier to just
// black-list caching these programs in all cases. See: anglebug.com/3619
// We also can't cache SkSL or GLSL if we were given a precompiled program, but there's not
// much point in doing so.
if (!cached && !primProc.isPathRendering() && !precompiledProgram) {
bool isSkSL = false;
if (fGpu->getContext()->priv().options().fShaderCacheStrategy ==
GrContextOptions::ShaderCacheStrategy::kSkSL) {
for (int i = 0; i < kGrShaderTypeCount; ++i) {
glsl[i] = GrShaderUtils::PrettyPrint(*sksl[i]);
}
isSkSL = true;
}
this->storeShaderInCache(inputs, programID, glsl, isSkSL, &settings);
}
return this->createProgram(programID);
}
void GrGLProgramBuilder::bindProgramResourceLocations(GrGLuint programID) {
fUniformHandler.bindUniformLocations(programID, fGpu->glCaps());
const GrGLCaps& caps = this->gpu()->glCaps();
if (fFS.hasCustomColorOutput() && caps.bindFragDataLocationSupport()) {
GL_CALL(BindFragDataLocation(programID, 0,
GrGLSLFragmentShaderBuilder::DeclaredColorOutputName()));
}
if (fFS.hasSecondaryOutput() && caps.shaderCaps()->mustDeclareFragmentShaderOutput()) {
GL_CALL(BindFragDataLocationIndexed(programID, 0, 1,
GrGLSLFragmentShaderBuilder::DeclaredSecondaryColorOutputName()));
}
// handle NVPR separable varyings
if (!fGpu->glCaps().shaderCaps()->pathRenderingSupport() ||
!fGpu->glPathRendering()->shouldBindFragmentInputs()) {
return;
}
int count = fVaryingHandler.fPathProcVaryingInfos.count();
for (int i = 0; i < count; ++i) {
GL_CALL(BindFragmentInputLocation(programID, i,
fVaryingHandler.fPathProcVaryingInfos[i].fVariable.c_str()));
fVaryingHandler.fPathProcVaryingInfos[i].fLocation = i;
}
}
bool GrGLProgramBuilder::checkLinkStatus(GrGLuint programID,
GrContextOptions::ShaderErrorHandler* errorHandler,
SkSL::String* sksl[], const SkSL::String glsl[]) {
GrGLint linked = GR_GL_INIT_ZERO;
GL_CALL(GetProgramiv(programID, GR_GL_LINK_STATUS, &linked));
if (!linked) {
SkSL::String allShaders;
if (sksl) {
allShaders.appendf("// Vertex SKSL\n%s\n", sksl[kVertex_GrShaderType]->c_str());
if (!sksl[kGeometry_GrShaderType]->empty()) {
allShaders.appendf("// Geometry SKSL\n%s\n", sksl[kGeometry_GrShaderType]->c_str());
}
allShaders.appendf("// Fragment SKSL\n%s\n", sksl[kFragment_GrShaderType]->c_str());
}
if (glsl) {
allShaders.appendf("// Vertex GLSL\n%s\n", glsl[kVertex_GrShaderType].c_str());
if (!glsl[kGeometry_GrShaderType].empty()) {
allShaders.appendf("// Geometry GLSL\n%s\n", glsl[kGeometry_GrShaderType].c_str());
}
allShaders.appendf("// Fragment GLSL\n%s\n", glsl[kFragment_GrShaderType].c_str());
}
GrGLint infoLen = GR_GL_INIT_ZERO;
GL_CALL(GetProgramiv(programID, GR_GL_INFO_LOG_LENGTH, &infoLen));
SkAutoMalloc log(sizeof(char)*(infoLen+1)); // outside if for debugger
if (infoLen > 0) {
// retrieve length even though we don't need it to workaround
// bug in chrome cmd buffer param validation.
GrGLsizei length = GR_GL_INIT_ZERO;
GL_CALL(GetProgramInfoLog(programID, infoLen+1, &length, (char*)log.get()));
}
errorHandler->compileError(allShaders.c_str(), infoLen > 0 ? (const char*)log.get() : "");
}
return SkToBool(linked);
}
void GrGLProgramBuilder::resolveProgramResourceLocations(GrGLuint programID, bool force) {
fUniformHandler.getUniformLocations(programID, fGpu->glCaps(), force);
// handle NVPR separable varyings
if (!fGpu->glCaps().shaderCaps()->pathRenderingSupport() ||
fGpu->glPathRendering()->shouldBindFragmentInputs()) {
return;
}
int count = fVaryingHandler.fPathProcVaryingInfos.count();
for (int i = 0; i < count; ++i) {
GrGLint location;
GL_CALL_RET(location, GetProgramResourceLocation(
programID,
GR_GL_FRAGMENT_INPUT,
fVaryingHandler.fPathProcVaryingInfos[i].fVariable.c_str()));
fVaryingHandler.fPathProcVaryingInfos[i].fLocation = location;
}
}
GrGLProgram* GrGLProgramBuilder::createProgram(GrGLuint programID) {
return new GrGLProgram(fGpu,
fUniformHandles,
programID,
fUniformHandler.fUniforms,
fUniformHandler.fSamplers,
fVaryingHandler.fPathProcVaryingInfos,
std::move(fGeometryProcessor),
std::move(fXferProcessor),
std::move(fFragmentProcessors),
fFragmentProcessorCnt,
std::move(fAttributes),
fVertexAttributeCnt,
fInstanceAttributeCnt,
fVertexStride,
fInstanceStride);
}
bool GrGLProgramBuilder::PrecompileProgram(GrGLPrecompiledProgram* precompiledProgram,
GrGLGpu* gpu,
const SkData& cachedData) {
SkReader32 reader(cachedData.data(), cachedData.size());
SkFourByteTag shaderType = reader.readU32();
if (shaderType != kSKSL_Tag) {
// TODO: Support GLSL, and maybe even program binaries, too?
return false;
}
const GrGLInterface* gl = gpu->glInterface();
auto errorHandler = gpu->getContext()->priv().getShaderErrorHandler();
GrGLuint programID;
GR_GL_CALL_RET(gl, programID, CreateProgram());
if (0 == programID) {
return false;
}
SkTDArray<GrGLuint> shadersToDelete;
SkSL::Program::Settings settings;
const GrGLCaps& caps = gpu->glCaps();
settings.fCaps = caps.shaderCaps();
settings.fSharpenTextures = gpu->getContext()->priv().options().fSharpenMipmappedTextures;
GrPersistentCacheUtils::ShaderMetadata meta;
meta.fSettings = &settings;
SkSL::String shaders[kGrShaderTypeCount];
SkSL::Program::Inputs inputs;
GrPersistentCacheUtils::UnpackCachedShaders(&reader, shaders, &inputs, 1, &meta);
auto compileShader = [&](SkSL::Program::Kind kind, const SkSL::String& sksl, GrGLenum type) {
SkSL::String glsl;
auto program = GrSkSLtoGLSL(gpu->glContext(), kind, sksl, settings, &glsl, errorHandler);
if (!program) {
return false;
}
if (GrGLuint shaderID = GrGLCompileAndAttachShader(gpu->glContext(), programID, type, glsl,
gpu->stats(), errorHandler)) {
shadersToDelete.push_back(shaderID);
return true;
} else {
return false;
}
};
if (!compileShader(SkSL::Program::kFragment_Kind,
shaders[kFragment_GrShaderType],
GR_GL_FRAGMENT_SHADER) ||
!compileShader(SkSL::Program::kVertex_Kind,
shaders[kVertex_GrShaderType],
GR_GL_VERTEX_SHADER) ||
(!shaders[kGeometry_GrShaderType].empty() &&
!compileShader(SkSL::Program::kGeometry_Kind,
shaders[kGeometry_GrShaderType],
GR_GL_GEOMETRY_SHADER))) {
cleanup_program(gpu, programID, shadersToDelete);
return false;
}
for (int i = 0; i < meta.fAttributeNames.count(); ++i) {
GR_GL_CALL(gpu->glInterface(), BindAttribLocation(programID, i,
meta.fAttributeNames[i].c_str()));
}
if (meta.fHasCustomColorOutput && caps.bindFragDataLocationSupport()) {
GR_GL_CALL(gpu->glInterface(), BindFragDataLocation(programID, 0,
GrGLSLFragmentShaderBuilder::DeclaredColorOutputName()));
}
if (meta.fHasSecondaryColorOutput && caps.shaderCaps()->mustDeclareFragmentShaderOutput()) {
GR_GL_CALL(gpu->glInterface(), BindFragDataLocationIndexed(programID, 0, 1,
GrGLSLFragmentShaderBuilder::DeclaredSecondaryColorOutputName()));
}
GR_GL_CALL(gpu->glInterface(), LinkProgram(programID));
GrGLint linked = GR_GL_INIT_ZERO;
GR_GL_CALL(gpu->glInterface(), GetProgramiv(programID, GR_GL_LINK_STATUS, &linked));
if (!linked) {
cleanup_program(gpu, programID, shadersToDelete);
return false;
}
cleanup_shaders(gpu, shadersToDelete);
precompiledProgram->fProgramID = programID;
precompiledProgram->fInputs = inputs;
return true;
}