/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "WebGLContext.h" #include "MozFramebuffer.h" #include "GLContext.h" #include "mozilla/CheckedInt.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_webgl.h" #include "mozilla/UniquePtrExtensions.h" #include "nsPrintfCString.h" #include "WebGLBuffer.h" #include "WebGLContextUtils.h" #include "WebGLFramebuffer.h" #include "WebGLProgram.h" #include "WebGLRenderbuffer.h" #include "WebGLShader.h" #include "WebGLTexture.h" #include "WebGLTransformFeedback.h" #include "WebGLVertexArray.h" #include namespace mozilla { // For a Tegra workaround. static const int MAX_DRAW_CALLS_SINCE_FLUSH = 100; //////////////////////////////////////// class ScopedResolveTexturesForDraw { struct TexRebindRequest { uint32_t texUnit; WebGLTexture* tex; }; WebGLContext* const mWebGL; std::vector mRebindRequests; public: ScopedResolveTexturesForDraw(WebGLContext* webgl, bool* const out_error); ~ScopedResolveTexturesForDraw(); }; static bool ValidateNoSamplingFeedback(const WebGLTexture& tex, const uint32_t sampledLevels, const WebGLFramebuffer* const fb, const uint32_t texUnit) { if (!fb) return true; const auto& texAttachments = fb->GetCompletenessInfo()->texAttachments; for (const auto& attach : texAttachments) { if (attach->Texture() != &tex) continue; const auto& srcBase = tex.Es3_level_base(); const auto srcLast = srcBase + sampledLevels - 1; const auto& dstLevel = attach->MipLevel(); if (MOZ_UNLIKELY(srcBase <= dstLevel && dstLevel <= srcLast)) { const auto& webgl = tex.mContext; const auto& texTargetStr = EnumString(tex.Target().get()); const auto& attachStr = EnumString(attach->mAttachmentPoint); webgl->ErrorInvalidOperation( "Texture level %u would be read by %s unit %u," " but written by framebuffer attachment %s," " which would be illegal feedback.", dstLevel, texTargetStr.c_str(), texUnit, attachStr.c_str()); return false; } } return true; } ScopedResolveTexturesForDraw::ScopedResolveTexturesForDraw( WebGLContext* webgl, bool* const out_error) : mWebGL(webgl) { const auto& fb = mWebGL->mBoundDrawFramebuffer; struct SamplerByTexUnit { uint8_t texUnit; const webgl::SamplerUniformInfo* sampler; }; Vector samplerByTexUnit; MOZ_ASSERT(mWebGL->mActiveProgramLinkInfo); const auto& samplerUniforms = mWebGL->mActiveProgramLinkInfo->samplerUniforms; for (const auto& pUniform : samplerUniforms) { const auto& uniform = *pUniform; const auto& texList = uniform.texListForType; const auto& uniformBaseType = uniform.texBaseType; for (const auto& texUnit : uniform.texUnits) { MOZ_ASSERT(texUnit < texList.Length()); { decltype(SamplerByTexUnit::sampler) prevSamplerForTexUnit = nullptr; for (const auto& cur : samplerByTexUnit) { if (cur.texUnit == texUnit) { prevSamplerForTexUnit = cur.sampler; } } if (!prevSamplerForTexUnit) { prevSamplerForTexUnit = &uniform; MOZ_RELEASE_ASSERT(samplerByTexUnit.append( SamplerByTexUnit{texUnit, prevSamplerForTexUnit})); } if (MOZ_UNLIKELY(&uniform.texListForType != &prevSamplerForTexUnit->texListForType)) { // Pointing to different tex lists means different types! const auto linkInfo = mWebGL->mActiveProgramLinkInfo; const auto LocInfoBySampler = [&](const webgl::SamplerUniformInfo* p) -> const webgl::LocationInfo* { for (const auto& pair : linkInfo->locationMap) { const auto& locInfo = pair.second; if (locInfo.samplerInfo == p) { return &locInfo; } } MOZ_CRASH("Can't find sampler location."); }; const auto& cur = *LocInfoBySampler(&uniform); const auto& prev = *LocInfoBySampler(prevSamplerForTexUnit); mWebGL->ErrorInvalidOperation( "Tex unit %u referenced by samplers of different types:" " %s (via %s) and %s (via %s).", texUnit, EnumString(cur.info.info.elemType).c_str(), cur.PrettyName().c_str(), EnumString(prev.info.info.elemType).c_str(), prev.PrettyName().c_str()); *out_error = true; return; } } const auto& tex = texList[texUnit]; if (!tex) continue; const auto& sampler = mWebGL->mBoundSamplers[texUnit]; const auto& samplingInfo = tex->GetSampleableInfo(sampler.get()); if (MOZ_UNLIKELY(!samplingInfo)) { // There was an error. *out_error = true; return; } if (MOZ_UNLIKELY(!samplingInfo->IsComplete())) { if (samplingInfo->incompleteReason) { const auto& targetName = GetEnumName(tex->Target().get()); mWebGL->GenerateWarning("%s at unit %u is incomplete: %s", targetName, texUnit, samplingInfo->incompleteReason); } mRebindRequests.push_back({texUnit, tex}); continue; } // We have more validation to do if we're otherwise complete: const auto& texBaseType = samplingInfo->usage->format->baseType; if (MOZ_UNLIKELY(texBaseType != uniformBaseType)) { const auto& targetName = GetEnumName(tex->Target().get()); const auto& srcType = ToString(texBaseType); const auto& dstType = ToString(uniformBaseType); mWebGL->ErrorInvalidOperation( "%s at unit %u is of type %s, but" " the shader samples as %s.", targetName, texUnit, srcType, dstType); *out_error = true; return; } if (MOZ_UNLIKELY(uniform.isShadowSampler != samplingInfo->isDepthTexCompare)) { const auto& targetName = GetEnumName(tex->Target().get()); mWebGL->ErrorInvalidOperation( "%s at unit %u is%s a depth texture" " with TEXTURE_COMPARE_MODE, but" " the shader sampler is%s a shadow" " sampler.", targetName, texUnit, samplingInfo->isDepthTexCompare ? "" : " not", uniform.isShadowSampler ? "" : " not"); *out_error = true; return; } if (MOZ_UNLIKELY(!ValidateNoSamplingFeedback(*tex, samplingInfo->levels, fb.get(), texUnit))) { *out_error = true; return; } } } const auto& gl = mWebGL->gl; for (const auto& itr : mRebindRequests) { gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit); GLuint incompleteTex = 0; // Tex 0 is always incomplete. const auto& overrideTex = webgl->mIncompleteTexOverride; if (overrideTex) { // In all but the simplest cases, this will be incomplete anyway, since // e.g. int-samplers need int-textures. This is useful for e.g. // dom-to-texture failures, though. incompleteTex = overrideTex->name; } gl->fBindTexture(itr.tex->Target().get(), incompleteTex); } } ScopedResolveTexturesForDraw::~ScopedResolveTexturesForDraw() { if (mRebindRequests.empty()) return; gl::GLContext* gl = mWebGL->gl; for (const auto& itr : mRebindRequests) { gl->fActiveTexture(LOCAL_GL_TEXTURE0 + itr.texUnit); gl->fBindTexture(itr.tex->Target().get(), itr.tex->mGLName); } gl->fActiveTexture(LOCAL_GL_TEXTURE0 + mWebGL->mActiveTexture); } //////////////////////////////////////// bool WebGLContext::ValidateStencilParamsForDrawCall() const { const auto stencilBits = [&]() -> uint8_t { if (!mStencilTestEnabled) return 0; if (!mBoundDrawFramebuffer) return mOptions.stencil ? 8 : 0; if (mBoundDrawFramebuffer->StencilAttachment().HasAttachment()) return 8; if (mBoundDrawFramebuffer->DepthStencilAttachment().HasAttachment()) return 8; return 0; }(); const uint32_t stencilMax = (1 << stencilBits) - 1; const auto fnMask = [&](const uint32_t x) { return x & stencilMax; }; const auto fnClamp = [&](const int32_t x) { return std::max(0, std::min(x, (int32_t)stencilMax)); }; bool ok = true; ok &= (fnMask(mStencilWriteMaskFront) == fnMask(mStencilWriteMaskBack)); ok &= (fnMask(mStencilValueMaskFront) == fnMask(mStencilValueMaskBack)); ok &= (fnClamp(mStencilRefFront) == fnClamp(mStencilRefBack)); if (!ok) { ErrorInvalidOperation( "Stencil front/back state must effectively match." " (before front/back comparison, WRITEMASK and VALUE_MASK" " are masked with (2^s)-1, and REF is clamped to" " [0, (2^s)-1], where `s` is the number of enabled stencil" " bits in the draw framebuffer)"); } return ok; } // - void WebGLContext::GenErrorIllegalUse(const GLenum useTarget, const uint32_t useId, const GLenum boundTarget, const uint32_t boundId) const { const auto fnName = [&](const GLenum target, const uint32_t id) { auto name = nsCString(EnumString(target).c_str()); if (id != static_cast(-1)) { name += nsPrintfCString("[%u]", id); } return name; }; const auto& useName = fnName(useTarget, useId); const auto& boundName = fnName(boundTarget, boundId); GenerateError(LOCAL_GL_INVALID_OPERATION, "Illegal use of buffer at %s" " while also bound to %s.", useName.BeginReading(), boundName.BeginReading()); } bool WebGLContext::ValidateBufferForNonTf(const WebGLBuffer& nonTfBuffer, const GLenum nonTfTarget, const uint32_t nonTfId) const { bool dupe = false; const auto& tfAttribs = mBoundTransformFeedback->mIndexedBindings; for (const auto& cur : tfAttribs) { dupe |= (&nonTfBuffer == cur.mBufferBinding.get()); } if (MOZ_LIKELY(!dupe)) return true; dupe = false; for (const auto tfId : IntegerRange(tfAttribs.size())) { const auto& tfBuffer = tfAttribs[tfId].mBufferBinding; if (&nonTfBuffer == tfBuffer) { dupe = true; GenErrorIllegalUse(nonTfTarget, nonTfId, LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, tfId); } } MOZ_ASSERT(dupe); return false; } bool WebGLContext::ValidateBuffersForTf( const WebGLTransformFeedback& tfo, const webgl::LinkedProgramInfo& linkInfo) const { size_t numUsed; switch (linkInfo.transformFeedbackBufferMode) { case LOCAL_GL_INTERLEAVED_ATTRIBS: numUsed = 1; break; case LOCAL_GL_SEPARATE_ATTRIBS: numUsed = linkInfo.active.activeTfVaryings.size(); break; default: MOZ_CRASH(); } std::vector tfBuffers; tfBuffers.reserve(numUsed); for (const auto i : IntegerRange(numUsed)) { tfBuffers.push_back({tfo.mIndexedBindings[i].mBufferBinding.get(), static_cast(i)}); } return ValidateBuffersForTf(tfBuffers); } bool WebGLContext::ValidateBuffersForTf( const std::vector& tfBuffers) const { bool dupe = false; const auto fnCheck = [&](const WebGLBuffer* const nonTf, const GLenum nonTfTarget, const uint32_t nonTfId) { for (const auto& tf : tfBuffers) { dupe |= (nonTf && tf.buffer == nonTf); } if (MOZ_LIKELY(!dupe)) return false; for (const auto& tf : tfBuffers) { if (nonTf && tf.buffer == nonTf) { dupe = true; GenErrorIllegalUse(LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, tf.id, nonTfTarget, nonTfId); } } return true; }; fnCheck(mBoundArrayBuffer.get(), LOCAL_GL_ARRAY_BUFFER, -1); fnCheck(mBoundCopyReadBuffer.get(), LOCAL_GL_COPY_READ_BUFFER, -1); fnCheck(mBoundCopyWriteBuffer.get(), LOCAL_GL_COPY_WRITE_BUFFER, -1); fnCheck(mBoundPixelPackBuffer.get(), LOCAL_GL_PIXEL_PACK_BUFFER, -1); fnCheck(mBoundPixelUnpackBuffer.get(), LOCAL_GL_PIXEL_UNPACK_BUFFER, -1); // fnCheck(mBoundTransformFeedbackBuffer.get(), // LOCAL_GL_TRANSFORM_FEEDBACK_BUFFER, -1); fnCheck(mBoundUniformBuffer.get(), LOCAL_GL_UNIFORM_BUFFER, -1); for (const auto i : IntegerRange(mIndexedUniformBufferBindings.size())) { const auto& cur = mIndexedUniformBufferBindings[i]; fnCheck(cur.mBufferBinding.get(), LOCAL_GL_UNIFORM_BUFFER, i); } fnCheck(mBoundVertexArray->mElementArrayBuffer.get(), LOCAL_GL_ELEMENT_ARRAY_BUFFER, -1); for (const auto i : IntegerRange(MaxVertexAttribs())) { const auto& binding = mBoundVertexArray->AttribBinding(i); fnCheck(binding.buffer.get(), LOCAL_GL_ARRAY_BUFFER, i); } return !dupe; } //////////////////////////////////////// template static bool DoSetsIntersect(const std::set& a, const std::set& b) { std::vector intersection; std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), std::back_inserter(intersection)); return !intersection.empty(); } template static size_t FindFirstOne(const std::bitset& bs) { MOZ_ASSERT(bs.any()); // We don't need this to be fast, so don't bother with CLZ intrinsics. for (const auto i : IntegerRange(N)) { if (bs[i]) return i; } return -1; } const webgl::CachedDrawFetchLimits* ValidateDraw(WebGLContext* const webgl, const GLenum mode, const uint32_t instanceCount) { if (!webgl->BindCurFBForDraw()) return nullptr; const auto& fb = webgl->mBoundDrawFramebuffer; if (fb) { const auto& info = *fb->GetCompletenessInfo(); const auto isF32WithBlending = info.isAttachmentF32 & webgl->mBlendEnabled; if (isF32WithBlending.any()) { if (!webgl->IsExtensionEnabled(WebGLExtensionID::EXT_float_blend)) { const auto first = FindFirstOne(isF32WithBlending); webgl->ErrorInvalidOperation( "Attachment %u is float32 with blending enabled, which requires " "EXT_float_blend.", uint32_t(first)); return nullptr; } webgl->WarnIfImplicit(WebGLExtensionID::EXT_float_blend); } } switch (mode) { case LOCAL_GL_TRIANGLES: case LOCAL_GL_TRIANGLE_STRIP: case LOCAL_GL_TRIANGLE_FAN: case LOCAL_GL_POINTS: case LOCAL_GL_LINE_STRIP: case LOCAL_GL_LINE_LOOP: case LOCAL_GL_LINES: break; default: webgl->ErrorInvalidEnumInfo("mode", mode); return nullptr; } if (!webgl->ValidateStencilParamsForDrawCall()) return nullptr; if (!webgl->mActiveProgramLinkInfo) { webgl->ErrorInvalidOperation("The current program is not linked."); return nullptr; } const auto& linkInfo = webgl->mActiveProgramLinkInfo; // - // Check UBO sizes. for (const auto i : IntegerRange(linkInfo->uniformBlocks.size())) { const auto& cur = linkInfo->uniformBlocks[i]; const auto& dataSize = cur.info.dataSize; const auto& binding = cur.binding; if (!binding) { webgl->ErrorInvalidOperation("Buffer for uniform block is null."); return nullptr; } const auto availByteCount = binding->ByteCount(); if (dataSize > availByteCount) { webgl->ErrorInvalidOperation( "Buffer for uniform block is smaller" " than UNIFORM_BLOCK_DATA_SIZE."); return nullptr; } if (!webgl->ValidateBufferForNonTf(binding->mBufferBinding, LOCAL_GL_UNIFORM_BUFFER, i)) return nullptr; } // - const auto& tfo = webgl->mBoundTransformFeedback; if (tfo && tfo->IsActiveAndNotPaused()) { if (fb) { const auto& info = *fb->GetCompletenessInfo(); if (info.isMultiview) { webgl->ErrorInvalidOperation( "Cannot render to multiview with transform feedback."); return nullptr; } } if (!webgl->ValidateBuffersForTf(*tfo, *linkInfo)) return nullptr; } // - const auto& fragOutputs = linkInfo->fragOutputs; const auto fnValidateFragOutputType = [&](const uint8_t loc, const webgl::TextureBaseType dstBaseType) { const auto itr = fragOutputs.find(loc); MOZ_DIAGNOSTIC_ASSERT(itr != fragOutputs.end()); const auto& info = itr->second; const auto& srcBaseType = info.baseType; if (MOZ_UNLIKELY(dstBaseType != srcBaseType)) { const auto& srcStr = ToString(srcBaseType); const auto& dstStr = ToString(dstBaseType); webgl->ErrorInvalidOperation( "Program frag output at location %u is type %s," " but destination draw buffer is type %s.", uint32_t(loc), srcStr, dstStr); return false; } return true; }; if (!webgl->mRasterizerDiscardEnabled) { uint8_t fbZLayerCount = 1; auto hasAttachment = std::bitset(1); auto drawBufferEnabled = std::bitset(); if (fb) { drawBufferEnabled = fb->DrawBufferEnabled(); const auto& info = *fb->GetCompletenessInfo(); fbZLayerCount = info.zLayerCount; hasAttachment = info.hasAttachment; } else { drawBufferEnabled[0] = (webgl->mDefaultFB_DrawBuffer0 == LOCAL_GL_BACK); } if (fbZLayerCount != linkInfo->zLayerCount) { webgl->ErrorInvalidOperation( "Multiview count mismatch: shader: %u, framebuffer: %u", uint32_t{linkInfo->zLayerCount}, uint32_t{fbZLayerCount}); return nullptr; } const auto writable = hasAttachment & drawBufferEnabled & webgl->mColorWriteMaskNonzero; if (writable.any()) { // Do we have any undefined outputs with real attachments that // aren't masked-out by color write mask or drawBuffers? const auto wouldWriteUndefined = ~linkInfo->hasOutput & writable; if (wouldWriteUndefined.any()) { const auto first = FindFirstOne(wouldWriteUndefined); webgl->ErrorInvalidOperation( "Program has no frag output at location %u, the" " destination draw buffer has an attached" " image, and its color write mask is not all false," " and DRAW_BUFFER%u is not NONE.", uint32_t(first), uint32_t(first)); return nullptr; } const auto outputWrites = linkInfo->hasOutput & writable; if (fb) { for (const auto& attach : fb->ColorDrawBuffers()) { const auto i = uint8_t(attach->mAttachmentPoint - LOCAL_GL_COLOR_ATTACHMENT0); if (!outputWrites[i]) continue; const auto& imageInfo = attach->GetImageInfo(); if (!imageInfo) continue; const auto& dstBaseType = imageInfo->mFormat->format->baseType; if (!fnValidateFragOutputType(i, dstBaseType)) return nullptr; } } else { if (outputWrites[0]) { if (!fnValidateFragOutputType(0, webgl::TextureBaseType::Float)) return nullptr; } } } } // - const auto fetchLimits = linkInfo->GetDrawFetchLimits(); if (!fetchLimits) return nullptr; if (instanceCount > fetchLimits->maxInstances) { webgl->ErrorInvalidOperation( "Instance fetch requires %u, but attribs only" " supply %u.", instanceCount, uint32_t(fetchLimits->maxInstances)); return nullptr; } if (tfo) { for (const auto& used : fetchLimits->usedBuffers) { MOZ_ASSERT(used.buffer); if (!webgl->ValidateBufferForNonTf(*used.buffer, LOCAL_GL_ARRAY_BUFFER, used.id)) return nullptr; } } // - webgl->RunContextLossTimer(); return fetchLimits; } //////////////////////////////////////// static uint32_t UsedVertsForTFDraw(GLenum mode, uint32_t vertCount) { uint8_t vertsPerPrim; switch (mode) { case LOCAL_GL_POINTS: vertsPerPrim = 1; break; case LOCAL_GL_LINES: vertsPerPrim = 2; break; case LOCAL_GL_TRIANGLES: vertsPerPrim = 3; break; default: MOZ_CRASH("`mode`"); } return vertCount / vertsPerPrim * vertsPerPrim; } class ScopedDrawWithTransformFeedback final { WebGLContext* const mWebGL; WebGLTransformFeedback* const mTFO; const bool mWithTF; uint32_t mUsedVerts; public: ScopedDrawWithTransformFeedback(WebGLContext* webgl, GLenum mode, uint32_t vertCount, uint32_t instanceCount, bool* const out_error) : mWebGL(webgl), mTFO(mWebGL->mBoundTransformFeedback), mWithTF(mTFO && mTFO->mIsActive && !mTFO->mIsPaused), mUsedVerts(0) { *out_error = false; if (!mWithTF) return; if (mode != mTFO->mActive_PrimMode) { mWebGL->ErrorInvalidOperation( "Drawing with transform feedback requires" " `mode` to match BeginTransformFeedback's" " `primitiveMode`."); *out_error = true; return; } const auto usedVertsPerInstance = UsedVertsForTFDraw(mode, vertCount); const auto usedVerts = CheckedInt(usedVertsPerInstance) * instanceCount; const auto remainingCapacity = mTFO->mActive_VertCapacity - mTFO->mActive_VertPosition; if (!usedVerts.isValid() || usedVerts.value() > remainingCapacity) { mWebGL->ErrorInvalidOperation( "Insufficient buffer capacity remaining for" " transform feedback."); *out_error = true; return; } mUsedVerts = usedVerts.value(); } void Advance() const { if (!mWithTF) return; mTFO->mActive_VertPosition += mUsedVerts; for (const auto& cur : mTFO->mIndexedBindings) { const auto& buffer = cur.mBufferBinding; if (buffer) { buffer->ResetLastUpdateFenceId(); } } } }; static bool HasInstancedDrawing(const WebGLContext& webgl) { return webgl.IsWebGL2() || webgl.IsExtensionEnabled(WebGLExtensionID::ANGLE_instanced_arrays); } //////////////////////////////////////// void WebGLContext::DrawArraysInstanced(const GLenum mode, const GLint first, const GLsizei iVertCount, const GLsizei instanceCount) { const FuncScope funcScope(*this, "drawArraysInstanced"); // AUTO_PROFILER_LABEL("WebGLContext::DrawArraysInstanced", GRAPHICS); if (IsContextLost()) return; const gl::GLContext::TlsScope inTls(gl); // - if (!ValidateNonNegative("first", first) || !ValidateNonNegative("vertCount", iVertCount) || !ValidateNonNegative("instanceCount", instanceCount)) { return; } const auto vertCount = AssertedCast(iVertCount); if (IsWebGL2() && !gl->IsSupported(gl::GLFeature::prim_restart_fixed)) { MOZ_ASSERT(gl->IsSupported(gl::GLFeature::prim_restart)); if (mPrimRestartTypeBytes != 0) { mPrimRestartTypeBytes = 0; // OSX appears to have severe perf issues with leaving this enabled. gl->fDisable(LOCAL_GL_PRIMITIVE_RESTART); } } // - const auto fetchLimits = ValidateDraw(this, mode, instanceCount); if (!fetchLimits) return; // - const auto totalVertCount_safe = CheckedInt(first) + vertCount; if (!totalVertCount_safe.isValid()) { ErrorOutOfMemory("`first+vertCount` out of range."); return; } auto totalVertCount = totalVertCount_safe.value(); if (vertCount && instanceCount && totalVertCount > fetchLimits->maxVerts) { ErrorInvalidOperation( "Vertex fetch requires %u, but attribs only supply %u.", totalVertCount, uint32_t(fetchLimits->maxVerts)); return; } if (vertCount > mMaxVertIdsPerDraw) { ErrorOutOfMemory( "Context's max vertCount is %u, but %u requested. " "[webgl.max-vert-ids-per-draw]", mMaxVertIdsPerDraw, vertCount); return; } // - bool error = false; // - const ScopedResolveTexturesForDraw scopedResolve(this, &error); if (error) return; const ScopedDrawWithTransformFeedback scopedTF(this, mode, vertCount, instanceCount, &error); if (error) return; // On MacOS (Intel?), `first` in glDrawArrays also increases where instanced // attribs are fetched from. There are two ways to fix this: // 1. DrawElements with a [0,1,2,...] index buffer, converting `first` to // `byteOffset` // 2. OR offset all non-instanced vertex attrib pointers back, and call // DrawArrays with first:0. // * But now gl_VertexID will be wrong! So we inject a uniform to offset it // back correctly. // #1 ought to be the lowest overhead for any first>0, // but DrawElements can't be used with transform-feedback, // so we need #2 to also work. // For now, only implement #2. const auto& activeAttribs = mActiveProgramLinkInfo->active.activeAttribs; auto driverFirst = first; if (first && mBug_DrawArraysInstancedUserAttribFetchAffectedByFirst) { // This is not particularly optimized, but we can if we need to. bool hasInstancedUserAttrib = false; bool hasVertexAttrib = false; for (const auto& a : activeAttribs) { if (a.location == -1) { if (a.name == "gl_VertexID") { hasVertexAttrib = true; } continue; } const auto& binding = mBoundVertexArray->AttribBinding(a.location); if (binding.layout.divisor) { hasInstancedUserAttrib = true; } else { hasVertexAttrib = true; } } if (hasInstancedUserAttrib && hasVertexAttrib) { driverFirst = 0; } } if (driverFirst != first) { for (const auto& a : activeAttribs) { if (a.location == -1) continue; const auto& binding = mBoundVertexArray->AttribBinding(a.location); if (binding.layout.divisor) continue; mBoundVertexArray->DoVertexAttrib(a.location, first); } gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, first); } { const auto whatDoesAttrib0Need = WhatDoesVertexAttrib0Need(); auto fakeVertCount = uint64_t(driverFirst) + vertCount; if (whatDoesAttrib0Need == WebGLVertexAttrib0Status::Default) { fakeVertCount = 0; } if (!(vertCount && instanceCount)) { fakeVertCount = 0; } auto undoAttrib0 = MakeScopeExit([&]() { MOZ_RELEASE_ASSERT(whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default); UndoFakeVertexAttrib0(); }); if (fakeVertCount) { if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) { error = true; undoAttrib0.release(); } } else { // No fake-verts needed. undoAttrib0.release(); } ScopedDrawCallWrapper wrapper(*this); if (vertCount && instanceCount) { if (HasInstancedDrawing(*this)) { gl->fDrawArraysInstanced(mode, driverFirst, vertCount, instanceCount); } else { MOZ_ASSERT(instanceCount == 1); gl->fDrawArrays(mode, driverFirst, vertCount); } } } if (driverFirst != first) { gl->fUniform1i(mActiveProgramLinkInfo->webgl_gl_VertexID_Offset, 0); for (const auto& a : activeAttribs) { if (a.location == -1) continue; const auto& binding = mBoundVertexArray->AttribBinding(a.location); if (binding.layout.divisor) continue; mBoundVertexArray->DoVertexAttrib(a.location, 0); } } Draw_cleanup(); scopedTF.Advance(); } //////////////////////////////////////// WebGLBuffer* WebGLContext::DrawElements_check(const GLsizei rawIndexCount, const GLenum type, const WebGLintptr byteOffset, const GLsizei instanceCount) { if (mBoundTransformFeedback && mBoundTransformFeedback->mIsActive && !mBoundTransformFeedback->mIsPaused) { ErrorInvalidOperation( "DrawElements* functions are incompatible with" " transform feedback."); return nullptr; } if (!ValidateNonNegative("vertCount", rawIndexCount) || !ValidateNonNegative("byteOffset", byteOffset) || !ValidateNonNegative("instanceCount", instanceCount)) { return nullptr; } const auto indexCount = uint32_t(rawIndexCount); uint8_t bytesPerIndex = 0; switch (type) { case LOCAL_GL_UNSIGNED_BYTE: bytesPerIndex = 1; break; case LOCAL_GL_UNSIGNED_SHORT: bytesPerIndex = 2; break; case LOCAL_GL_UNSIGNED_INT: if (IsWebGL2() || IsExtensionEnabled(WebGLExtensionID::OES_element_index_uint)) { bytesPerIndex = 4; } break; } if (!bytesPerIndex) { ErrorInvalidEnumInfo("type", type); return nullptr; } if (byteOffset % bytesPerIndex != 0) { ErrorInvalidOperation( "`byteOffset` must be a multiple of the size of `type`"); return nullptr; } //// if (IsWebGL2() && !gl->IsSupported(gl::GLFeature::prim_restart_fixed)) { MOZ_ASSERT(gl->IsSupported(gl::GLFeature::prim_restart)); if (mPrimRestartTypeBytes != bytesPerIndex) { mPrimRestartTypeBytes = bytesPerIndex; const uint32_t ones = UINT32_MAX >> (32 - 8 * mPrimRestartTypeBytes); gl->fEnable(LOCAL_GL_PRIMITIVE_RESTART); gl->fPrimitiveRestartIndex(ones); } } //// // Index fetching const auto& indexBuffer = mBoundVertexArray->mElementArrayBuffer; if (!indexBuffer) { ErrorInvalidOperation("Index buffer not bound."); return nullptr; } const size_t availBytes = indexBuffer->ByteLength(); const auto availIndices = AvailGroups(availBytes, byteOffset, bytesPerIndex, bytesPerIndex); if (instanceCount && indexCount > availIndices) { ErrorInvalidOperation("Index buffer too small."); return nullptr; } return indexBuffer.get(); } static void HandleDrawElementsErrors( WebGLContext* webgl, gl::GLContext::LocalErrorScope& errorScope) { const auto err = errorScope.GetError(); if (err == LOCAL_GL_INVALID_OPERATION) { webgl->ErrorInvalidOperation( "Driver rejected indexed draw call, possibly" " due to out-of-bounds indices."); return; } MOZ_ASSERT(!err); if (err) { webgl->ErrorImplementationBug( "Unexpected driver error during indexed draw" " call. Please file a bug."); return; } } void WebGLContext::DrawElementsInstanced(const GLenum mode, const GLsizei iIndexCount, const GLenum type, const WebGLintptr byteOffset, const GLsizei instanceCount) { const FuncScope funcScope(*this, "drawElementsInstanced"); // AUTO_PROFILER_LABEL("WebGLContext::DrawElementsInstanced", GRAPHICS); if (IsContextLost()) return; const gl::GLContext::TlsScope inTls(gl); const auto indexBuffer = DrawElements_check(iIndexCount, type, byteOffset, instanceCount); if (!indexBuffer) return; const auto indexCount = AssertedCast(iIndexCount); // - const auto fetchLimits = ValidateDraw(this, mode, instanceCount); if (!fetchLimits) return; const auto whatDoesAttrib0Need = WhatDoesVertexAttrib0Need(); uint64_t fakeVertCount = 0; if (whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default) { fakeVertCount = fetchLimits->maxVerts; } if (!indexCount || !instanceCount) { fakeVertCount = 0; } if (fakeVertCount == UINT64_MAX) { // Ok well that's too many! const auto exactMaxVertId = indexBuffer->GetIndexedFetchMaxVert(type, byteOffset, indexCount); MOZ_RELEASE_ASSERT(exactMaxVertId); fakeVertCount = uint32_t{*exactMaxVertId}; fakeVertCount += 1; } // - { uint64_t indexCapacity = indexBuffer->ByteLength(); switch (type) { case LOCAL_GL_UNSIGNED_BYTE: break; case LOCAL_GL_UNSIGNED_SHORT: indexCapacity /= 2; break; case LOCAL_GL_UNSIGNED_INT: indexCapacity /= 4; break; } uint32_t maxVertId = 0; const auto isFetchValid = [&]() { if (!indexCount || !instanceCount) return true; const auto globalMaxVertId = indexBuffer->GetIndexedFetchMaxVert(type, 0, indexCapacity); if (!globalMaxVertId) return true; if (globalMaxVertId.value() < fetchLimits->maxVerts) return true; const auto exactMaxVertId = indexBuffer->GetIndexedFetchMaxVert(type, byteOffset, indexCount); maxVertId = exactMaxVertId.value(); return maxVertId < fetchLimits->maxVerts; }(); if (!isFetchValid) { ErrorInvalidOperation( "Indexed vertex fetch requires %u vertices, but" " attribs only supply %u.", maxVertId + 1, uint32_t(fetchLimits->maxVerts)); return; } } if (indexCount > mMaxVertIdsPerDraw) { ErrorOutOfMemory( "Context's max indexCount is %u, but %u requested. " "[webgl.max-vert-ids-per-draw]", mMaxVertIdsPerDraw, indexCount); return; } // - bool error = false; // - auto undoAttrib0 = MakeScopeExit([&]() { MOZ_RELEASE_ASSERT(whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default); UndoFakeVertexAttrib0(); }); if (fakeVertCount) { if (!DoFakeVertexAttrib0(fakeVertCount, whatDoesAttrib0Need)) { error = true; undoAttrib0.release(); } } else { // No fake-verts needed. undoAttrib0.release(); } // - const ScopedResolveTexturesForDraw scopedResolve(this, &error); if (error) return; { ScopedDrawCallWrapper wrapper(*this); { UniquePtr errorScope; if (MOZ_UNLIKELY(gl->IsANGLE() && gl->mDebugFlags & gl::GLContext::DebugFlagAbortOnError)) { // ANGLE does range validation even when it doesn't need to. // With MOZ_GL_ABORT_ON_ERROR, we need to catch it or hit assertions. errorScope.reset(new gl::GLContext::LocalErrorScope(*gl)); } if (indexCount && instanceCount) { if (HasInstancedDrawing(*this)) { gl->fDrawElementsInstanced(mode, indexCount, type, reinterpret_cast(byteOffset), instanceCount); } else { MOZ_ASSERT(instanceCount == 1); gl->fDrawElements(mode, indexCount, type, reinterpret_cast(byteOffset)); } } if (errorScope) { HandleDrawElementsErrors(this, *errorScope); } } } Draw_cleanup(); } //////////////////////////////////////// void WebGLContext::Draw_cleanup() { if (gl->WorkAroundDriverBugs()) { if (gl->Renderer() == gl::GLRenderer::Tegra) { mDrawCallsSinceLastFlush++; if (mDrawCallsSinceLastFlush >= MAX_DRAW_CALLS_SINCE_FLUSH) { gl->fFlush(); mDrawCallsSinceLastFlush = 0; } } } // Let's check for a really common error: Viewport is larger than the actual // destination framebuffer. uint32_t destWidth; uint32_t destHeight; if (mBoundDrawFramebuffer) { const auto& info = mBoundDrawFramebuffer->GetCompletenessInfo(); destWidth = info->width; destHeight = info->height; } else { destWidth = mDefaultFB->mSize.width; destHeight = mDefaultFB->mSize.height; } if (mViewportWidth > int32_t(destWidth) || mViewportHeight > int32_t(destHeight)) { if (!mAlreadyWarnedAboutViewportLargerThanDest) { GenerateWarning( "Drawing to a destination rect smaller than the viewport" " rect. (This warning will only be given once)"); mAlreadyWarnedAboutViewportLargerThanDest = true; } } } WebGLVertexAttrib0Status WebGLContext::WhatDoesVertexAttrib0Need() const { MOZ_ASSERT(mCurrentProgram); MOZ_ASSERT(mActiveProgramLinkInfo); bool legacyAttrib0 = mNeedsLegacyVertexAttrib0Handling; if (gl->WorkAroundDriverBugs() && kIsMacOS) { // Also programs with no attribs: // conformance/attribs/gl-vertex-attrib-unconsumed-out-of-bounds.html const auto& activeAttribs = mActiveProgramLinkInfo->active.activeAttribs; bool hasNonInstancedUserAttrib = false; for (const auto& a : activeAttribs) { if (a.location == -1) continue; const auto& layout = mBoundVertexArray->AttribBinding(a.location).layout; if (layout.divisor == 0) { hasNonInstancedUserAttrib = true; } } legacyAttrib0 |= !hasNonInstancedUserAttrib; } if (!legacyAttrib0) return WebGLVertexAttrib0Status::Default; MOZ_RELEASE_ASSERT(mMaybeNeedsLegacyVertexAttrib0Handling, "Invariant need because this turns on index buffer " "validation, needed for fake-attrib0."); if (!mActiveProgramLinkInfo->attrib0Active) { // Attrib0 unused, so just ensure that the legacy code has enough buffer. return WebGLVertexAttrib0Status::EmulatedUninitializedArray; } const auto& isAttribArray0Enabled = mBoundVertexArray->AttribBinding(0).layout.isArray; return isAttribArray0Enabled ? WebGLVertexAttrib0Status::Default : WebGLVertexAttrib0Status::EmulatedInitializedArray; } bool WebGLContext::DoFakeVertexAttrib0( const uint64_t fakeVertexCount, const WebGLVertexAttrib0Status whatDoesAttrib0Need) { MOZ_ASSERT(fakeVertexCount); MOZ_RELEASE_ASSERT(whatDoesAttrib0Need != WebGLVertexAttrib0Status::Default); if (gl->WorkAroundDriverBugs() && gl->IsMesa()) { // Padded/strided to vec4, so 4x4bytes. const auto effectiveVertAttribBytes = CheckedInt(fakeVertexCount) * 4 * 4; if (!effectiveVertAttribBytes.isValid()) { ErrorOutOfMemory("`offset + count` too large for Mesa."); return false; } } if (!mAlreadyWarnedAboutFakeVertexAttrib0) { GenerateWarning( "Drawing without vertex attrib 0 array enabled forces the browser " "to do expensive emulation work when running on desktop OpenGL " "platforms, for example on Mac. It is preferable to always draw " "with vertex attrib 0 array enabled, by using bindAttribLocation " "to bind some always-used attribute to location 0."); mAlreadyWarnedAboutFakeVertexAttrib0 = true; } gl->fEnableVertexAttribArray(0); if (!mFakeVertexAttrib0BufferObject) { gl->fGenBuffers(1, &mFakeVertexAttrib0BufferObject); mFakeVertexAttrib0BufferObjectSize = 0; } gl->fBindBuffer(LOCAL_GL_ARRAY_BUFFER, mFakeVertexAttrib0BufferObject); //// switch (mGenericVertexAttribTypes[0]) { case webgl::AttribBaseType::Boolean: case webgl::AttribBaseType::Float: gl->fVertexAttribPointer(0, 4, LOCAL_GL_FLOAT, false, 0, 0); break; case webgl::AttribBaseType::Int: gl->fVertexAttribIPointer(0, 4, LOCAL_GL_INT, 0, 0); break; case webgl::AttribBaseType::Uint: gl->fVertexAttribIPointer(0, 4, LOCAL_GL_UNSIGNED_INT, 0, 0); break; } //// const auto maxFakeVerts = StaticPrefs::webgl_fake_verts_max(); if (fakeVertexCount > maxFakeVerts) { ErrorOutOfMemory( "Draw requires faking a vertex attrib 0 array, but required vert count" " (%" PRIu64 ") is more than webgl.fake-verts.max (%u).", fakeVertexCount, maxFakeVerts); return false; } const auto bytesPerVert = sizeof(mFakeVertexAttrib0Data); const auto checked_dataSize = CheckedInt(fakeVertexCount) * bytesPerVert; if (!checked_dataSize.isValid()) { ErrorOutOfMemory( "Integer overflow trying to construct a fake vertex attrib 0" " array for a draw-operation with %" PRIu64 " vertices. Try" " reducing the number of vertices.", fakeVertexCount); return false; } const auto dataSize = checked_dataSize.value(); if (mFakeVertexAttrib0BufferObjectSize < dataSize) { gl::GLContext::LocalErrorScope errorScope(*gl); gl->fBufferData(LOCAL_GL_ARRAY_BUFFER, dataSize, nullptr, LOCAL_GL_DYNAMIC_DRAW); const auto err = errorScope.GetError(); if (err) { ErrorOutOfMemory( "Failed to allocate fake vertex attrib 0 data: %zi bytes", dataSize); return false; } mFakeVertexAttrib0BufferObjectSize = dataSize; mFakeVertexAttrib0DataDefined = false; } if (whatDoesAttrib0Need == WebGLVertexAttrib0Status::EmulatedUninitializedArray) return true; //// if (mFakeVertexAttrib0DataDefined && memcmp(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert) == 0) { return true; } //// const auto data = UniqueBuffer::Take(malloc(dataSize)); if (!data) { ErrorOutOfMemory("Failed to allocate fake vertex attrib 0 array."); return false; } auto itr = (uint8_t*)data.get(); const auto itrEnd = itr + dataSize; while (itr != itrEnd) { memcpy(itr, mGenericVertexAttrib0Data, bytesPerVert); itr += bytesPerVert; } { gl::GLContext::LocalErrorScope errorScope(*gl); gl->fBufferSubData(LOCAL_GL_ARRAY_BUFFER, 0, dataSize, data.get()); const auto err = errorScope.GetError(); if (err) { ErrorOutOfMemory("Failed to upload fake vertex attrib 0 data."); return false; } } //// memcpy(mFakeVertexAttrib0Data, mGenericVertexAttrib0Data, bytesPerVert); mFakeVertexAttrib0DataDefined = true; return true; } void WebGLContext::UndoFakeVertexAttrib0() { static_assert(IsBufferTargetLazilyBound(LOCAL_GL_ARRAY_BUFFER)); mBoundVertexArray->DoVertexAttrib(0); } } // namespace mozilla