diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 42eadf0b2..7298d16e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -344,8 +344,9 @@ jobs: # windows opengles tests - name: Run Tests (opengles) if: steps.vars.outputs.arch != 'ARM64' && steps.vars.outputs.compatname != '-compat' + env: + LOVE_GRAPHICS_USE_OPENGLES: 1 run: | - $ENV:LOVE_GRAPHICS_USE_OPENGLES=1 powershell.exe ./install/lovec.exe ./megasource/libs/love/testing/main.lua --runAllTests --isRunner - name: Love Test Report (opengles) id: report2 diff --git a/testing/classes/TestMethod.lua b/testing/classes/TestMethod.lua index f4f8688d6..2061a8187 100644 --- a/testing/classes/TestMethod.lua +++ b/testing/classes/TestMethod.lua @@ -367,11 +367,19 @@ TestMethod = { end, + -- @method - TestMethod:waitFrames() + -- @desc - yields the method for x amount of frames + -- @param {number} frames - no. frames to wait + -- @return {nil} waitFrames = function(self, frames) - for i=1,frames do coroutine.yield() end + for _=1,frames do coroutine.yield() end end, + -- @method - TestMethod:waitSeconds() + -- @desc - yields the method for x amount of seconds + -- @param {number} seconds - no. seconds to wait + -- @return {nil} waitSeconds = function(self, seconds) local start = love.timer.getTime() while love.timer.getTime() < start + seconds do diff --git a/testing/conf.lua b/testing/conf.lua index 787650a49..a954b3119 100644 --- a/testing/conf.lua +++ b/testing/conf.lua @@ -1,4 +1,5 @@ function love.conf(t) + print("love.conf") t.console = true t.window.name = 'love.test' t.window.width = 360 @@ -8,3 +9,13 @@ function love.conf(t) t.window.stencil = true t.renderers = {"opengl"} end + +-- custom crash message here to catch anything that might occur with modules +-- loading before we hit main.lua +local function error_printer(msg, layer) + print((debug.traceback("Error: " .. tostring(msg), 1+(layer or 1)):gsub("\n[^\n]+$", ""))) +end +function love.errorhandler(msg) + msg = tostring(msg) + error_printer(msg, 2) +end diff --git a/testing/readme.md b/testing/readme.md index dab1cd7cb..9b328023d 100644 --- a/testing/readme.md +++ b/testing/readme.md @@ -114,14 +114,7 @@ For sanity-checking, if it's currently not covered or it's not possible to test ## Todo If you would like to contribute to the test suite please raise a PR with the main [love-test](https://github.com/ellraiser/love-test) repo. -The following items are all the things still outstanding, expanding on any existing tests is also very welcome! -- [ ] check for any 12.0 methods in the changelog not yet covered in the test suite -- [ ] add BMfont alt. tests for font class tests (Rasterizer + GlyphData) -- [ ] graphics.isCompressed() should have an example of all compressed files -- [ ] graphics.Mesh should have some graphical tests ideally to check vertex settings w/ shaders -- [ ] ability to test loading different combinations of modules if needed -- [ ] more scenario based tests similar to some of the obj class tests -- [ ] performance tests? need to discuss what + how +There is a list of outstanding methods that require test coverage in `todo.md`, expanding on any existing tests is also very welcome! --- diff --git a/testing/resources/pop.ogg b/testing/resources/pop.ogg new file mode 100644 index 000000000..fb0a82a7b Binary files /dev/null and b/testing/resources/pop.ogg differ diff --git a/testing/resources/tone.ogg b/testing/resources/tone.ogg new file mode 100644 index 000000000..1e7156e47 Binary files /dev/null and b/testing/resources/tone.ogg differ diff --git a/testing/tests/audio.lua b/testing/tests/audio.lua index 39133ab91..24eb7ea0a 100644 --- a/testing/tests/audio.lua +++ b/testing/tests/audio.lua @@ -299,6 +299,20 @@ love.test.audio.getOrientation = function(test) end +-- love.audio.getPlaybackDevice +love.test.audio.getPlaybackDevice = function(test) + test:assertNotNil(love.audio.getPlaybackDevice) + test:assertNotNil(love.audio.getPlaybackDevice()) +end + + +-- love.audio.getPlaybackDevices +love.test.audio.getPlaybackDevices = function(test) + test:assertNotNil(love.audio.getPlaybackDevices) + test:assertGreaterEqual(0, #love.audio.getPlaybackDevices(), 'check table') +end + + -- love.audio.getPosition -- @NOTE is there an expected default listener pos? love.test.audio.getPosition = function(test) @@ -442,6 +456,52 @@ love.test.audio.setOrientation = function(test) end +-- love.audio.setPlaybackDevice +love.test.audio.setPlaybackDevice = function(test) + -- check method + test:assertNotNil(love.audio.setPlaybackDevice) + -- check blank string name + local success1, msg1 = love.audio.setPlaybackDevice('') + -- check invalid name + local success2, msg2 = love.audio.setPlaybackDevice('loveFM') + -- check setting already set + local success3, msg3 = love.audio.setPlaybackDevice(love.audio.getPlaybackDevice()) -- current name + -- rn on macos all 3 return false + -- whereas linux/windows return true for blank/current, which is expected + -- as openalsoft treats blank as current + if love.system.getOS() == 'OS X' then + test:assertFalse(success1, 'check blank device fails') + test:assertFalse(success2, 'check invalid device fails') + test:assertFalse(success3, 'check existing device fails') + else + test:assertTrue(success1, 'check blank device is fine') + test:assertFalse(success2, 'check invalid device fails') + test:assertTrue(success3, 'check existing device is fine') + end + -- if other devices to play with lets set a different one + local devices = love.audio.getPlaybackDevices() + if #devices > 1 then + local another = '' + local current = love.audio.getPlaybackDevice() + for a=1,#devices do + if devices[a] ~= current then + another = devices[a] + break + end + end + if another ~= '' then + -- check setting new device + local success4, msg4 = love.audio.setPlaybackDevice(another) + test:assertTrue(success4, 'check setting different device') + -- check resetting to default + local success5, msg5 = love.audio.setPlaybackDevice() + test:assertTrue(success5, 'check resetting') + test:assertEquals(current, love.audio.getPlaybackDevice()) + end + end +end + + -- love.audio.setPosition love.test.audio.setPosition = function(test) -- check setting position vals are returned diff --git a/testing/tests/data.lua b/testing/tests/data.lua index 21b66b609..725d25f99 100644 --- a/testing/tests/data.lua +++ b/testing/tests/data.lua @@ -33,6 +33,10 @@ love.test.data.ByteData = function(test) test:assertEquals('o', byte5) end + -- check overwriting the byte data string + data:setString('love!', 5) + test:assertEquals('hellolove!', data:getString(), 'check change string') + end diff --git a/testing/tests/filesystem.lua b/testing/tests/filesystem.lua index 55e112553..c7d76e6f6 100644 --- a/testing/tests/filesystem.lua +++ b/testing/tests/filesystem.lua @@ -187,6 +187,27 @@ love.test.filesystem.getDirectoryItems = function(test) end +-- love.filesystem.getFullCommonPath +love.test.filesystem.getFullCommonPath = function(test) + -- check standard paths + local appsavedir = love.filesystem.getFullCommonPath('appsavedir') + local appdocuments = love.filesystem.getFullCommonPath('appdocuments') + local userhome = love.filesystem.getFullCommonPath('userhome') + local userappdata = love.filesystem.getFullCommonPath('userappdata') + local userdesktop = love.filesystem.getFullCommonPath('userdesktop') + local userdocuments = love.filesystem.getFullCommonPath('userdocuments') + test:assertNotNil(appsavedir) + test:assertNotNil(appdocuments) + test:assertNotNil(userhome) + test:assertNotNil(userappdata) + test:assertNotNil(userdesktop) + test:assertNotNil(userdocuments) + -- check invalid path + local ok = pcall(love.filesystem.getFullCommonPath, 'fakepath') + test:assertFalse(ok, 'check invalid common path') +end + + -- love.filesystem.getIdentity love.test.filesystem.getIdentity = function(test) -- check setting identity matches @@ -263,6 +284,7 @@ love.test.filesystem.getInfo = function(test) test:assertEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt', 'directory'), 'check not directory') test:assertNotEquals(nil, love.filesystem.getInfo('foo/bar/file2.txt'), 'check info not nil') test:assertEquals(love.filesystem.getInfo('foo/bar/file2.txt').size, 5, 'check info size match') + test:assertFalse(love.filesystem.getInfo('foo/bar/file2.txt').readonly, 'check readonly') -- @TODO test modified timestamp from info.modtime? -- cleanup love.filesystem.remove('foo/bar/file2.txt') @@ -300,12 +322,16 @@ love.test.filesystem.load = function(test) love.filesystem.write('test1.lua', 'function test()\nreturn 1\nend\nreturn test()') love.filesystem.write('test2.lua', 'function test()\nreturn 1') -- check file that doesn't exist - local chunk, errormsg = love.filesystem.load('faker.lua') - test:assertEquals(nil, chunk, 'check file doesnt exist') - -- check valid lua file - chunk, errormsg = love.filesystem.load('test1.lua') - test:assertEquals(nil, errormsg, 'check no error message') - test:assertEquals(1, chunk(), 'check lua file runs') + local chunk1, errormsg1 = love.filesystem.load('faker.lua', 'b') + test:assertEquals(nil, chunk1, 'check file doesnt exist') + -- check valid lua file (text load) + local chunk2, errormsg2 = love.filesystem.load('test1.lua', 't') + test:assertEquals(nil, errormsg2, 'check no error message') + test:assertEquals(1, chunk2(), 'check lua file runs') + -- check valid lua file (any load) + local chunk4, errormsg4 = love.filesystem.load('test1.lua', 'bt') + test:assertEquals(nil, errormsg2, 'check no error message') + test:assertEquals(1, chunk4(), 'check lua file runs') -- check invalid lua file local ok, chunk, err = pcall(love.filesystem.load, 'test2.lua') test:assertFalse(ok, 'check invalid lua file') @@ -334,6 +360,88 @@ love.test.filesystem.mount = function(test) end +-- love.filesystem.mountFullPath +love.test.filesystem.mountFullPath = function(test) + -- mount something in the working directory + local mount = love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') + test:assertTrue(mount, 'check can mount') + -- check reading file through mounted path label + local contents, _ = love.filesystem.read('tests/audio.lua') + test:assertNotEquals(nil, contents) + local unmount = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') + test:assertTrue(unmount, 'reset mount') +end + + +-- love.filesystem.unmountFullPath +love.test.filesystem.unmountFullPath = function(test) + -- try unmounting something we never mounted + local unmount1 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/faker') + test:assertFalse(unmount1, 'check not mounted to start with') + -- mount something to unmount after + love.filesystem.mountFullPath(love.filesystem.getSource() .. '/tests', 'tests', 'read') + local unmount2 = love.filesystem.unmountFullPath(love.filesystem.getSource() .. '/tests') + test:assertTrue(unmount2, 'check unmounted') +end + + +-- love.filesystem.mountCommonPath +love.test.filesystem.mountCommonPath = function(test) + -- check if we can mount all the expected paths + local mount1 = love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') + local mount2 = love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') + local mount3 = love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') + local mount4 = love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') + -- userdesktop isnt valid on linux + if love.system.getOS() ~= 'Linux' then + local mount5 = love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') + test:assertTrue(mount5, 'check mount userdesktop') + end + local mount6 = love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') + local ok = pcall(love.filesystem.mountCommonPath, 'fakepath', 'fake', 'readwrite') + test:assertTrue(mount1, 'check mount appsavedir') + test:assertTrue(mount2, 'check mount appdocuments') + test:assertTrue(mount3, 'check mount userhome') + test:assertTrue(mount4, 'check mount userappdata') + test:assertTrue(mount6, 'check mount userdocuments') + test:assertFalse(ok, 'check mount invalid common path fails') +end + + +-- love.filesystem.unmountCommonPath +--love.test.filesystem.unmountCommonPath = function(test) +-- -- check unmounting invalid +-- local ok = pcall(love.filesystem.unmountCommonPath, 'fakepath') +-- test:assertFalse(ok, 'check unmount invalid common path') +-- -- check mounting valid paths +-- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'read') +-- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'read') +-- love.filesystem.mountCommonPath('userhome', 'userhome', 'read') +-- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'read') +-- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'read') +-- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'read') +-- local unmount1 = love.filesystem.unmountCommonPath('appsavedir') +-- local unmount2 = love.filesystem.unmountCommonPath('appdocuments') +-- local unmount3 = love.filesystem.unmountCommonPath('userhome') +-- local unmount4 = love.filesystem.unmountCommonPath('userappdata') +-- local unmount5 = love.filesystem.unmountCommonPath('userdesktop') +-- local unmount6 = love.filesystem.unmountCommonPath('userdocuments') +-- test:assertTrue(unmount1, 'check unmount appsavedir') +-- test:assertTrue(unmount2, 'check unmount appdocuments') +-- test:assertTrue(unmount3, 'check unmount userhome') +-- test:assertTrue(unmount4, 'check unmount userappdata') +-- test:assertTrue(unmount5, 'check unmount userdesktop') +-- test:assertTrue(unmount6, 'check unmount userdocuments') +-- -- remount or future tests fail +-- love.filesystem.mountCommonPath('appsavedir', 'appsavedir', 'readwrite') +-- love.filesystem.mountCommonPath('appdocuments', 'appdocuments', 'readwrite') +-- love.filesystem.mountCommonPath('userhome', 'userhome', 'readwrite') +-- love.filesystem.mountCommonPath('userappdata', 'userappdata', 'readwrite') +-- love.filesystem.mountCommonPath('userdesktop', 'userdesktop', 'readwrite') +-- love.filesystem.mountCommonPath('userdocuments', 'userdocuments', 'readwrite') +--end + + -- love.filesystem.openFile -- @NOTE this is just basic nil checking, objs have their own test method love.test.filesystem.openFile = function(test) diff --git a/testing/tests/graphics.lua b/testing/tests/graphics.lua index da473da87..bc338b94d 100644 --- a/testing/tests/graphics.lua +++ b/testing/tests/graphics.lua @@ -113,6 +113,8 @@ love.test.graphics.Canvas = function(test) debugname = 'testcanvas' }) test:assertObject(canvas) + test:assertTrue(canvas:isCanvas(), 'check is canvas') + test:assertFalse(canvas:isComputeWritable(), 'check not compute writable') -- check dpi test:assertEquals(love.graphics.getDPIScale(), canvas:getDPIScale(), 'check dpi scale') @@ -216,6 +218,16 @@ love.test.graphics.Canvas = function(test) dcanvas:setDepthSampleMode('equal') test:assertEquals('equal', dcanvas:getDepthSampleMode(), 'check depth sample mode set') + -- check compute writeable (wont work on opengl mac) + if love.graphics.getSupported().glsl4 then + local ccanvas = love.graphics.newCanvas(100, 100, { + type = '2d', + format = 'rgba8', + computewrite = true + }) + test:assertTrue(ccanvas:isComputeWritable()) + end + end @@ -303,6 +315,8 @@ love.test.graphics.Image = function(test) mipmaps = true }) test:assertObject(image) + test:assertFalse(image:isCanvas(), 'check not canvas') + test:assertFalse(image:isComputeWritable(), 'check not compute writable') -- check dpi test:assertEquals(love.graphics.getDPIScale(), image:getDPIScale(), 'check dpi scale') @@ -1536,6 +1550,11 @@ love.test.graphics.captureScreenshot = function(test) -- need to wait until end of the frame for the screenshot test:assertNotNil(love.filesystem.openFile('example-screenshot.png', 'r')) love.filesystem.remove('example-screenshot.png') + -- test callback version + love.graphics.captureScreenshot(function (idata) + test:assertNotEquals(nil, idata, 'check we have image data') + end) + test:waitFrames(10) end diff --git a/testing/tests/image.lua b/testing/tests/image.lua index fa9f4cbd2..db98882d4 100644 --- a/testing/tests/image.lua +++ b/testing/tests/image.lua @@ -32,6 +32,11 @@ love.test.image.CompressedImageData = function(test) -- check mipmap count test:assertEquals(7, idata:getMipmapCount(), 'check mipmap count') + -- check linear + test:assertFalse(idata:isLinear(), 'check not linear') + idata:setLinear(true) + test:assertTrue(idata:isLinear(), 'check now linear') + end @@ -78,12 +83,24 @@ love.test.image.ImageData = function(test) local r2, g2, b2 = idata:getPixel(25, 25) test:assertEquals(1, r2+g2+b2, 'check set to red') - -- check encoding to an image + -- check encoding to an image (png) idata:encode('png', 'test-encode.png') - local read = love.filesystem.openFile('test-encode.png', 'r') - test:assertNotNil(read) + local read1 = love.filesystem.openFile('test-encode.png', 'r') + test:assertNotNil(read1) love.filesystem.remove('test-encode.png') + -- check encoding to an image (exr) + local edata = love.image.newImageData(100, 100, 'r16f') + edata:encode('exr', 'test-encode.exr') + local read2 = love.filesystem.openFile('test-encode.exr', 'r') + test:assertNotNil(read2) + love.filesystem.remove('test-encode.exr') + + -- check linear + test:assertFalse(idata:isLinear(), 'check not linear') + idata:setLinear(true) + test:assertTrue(idata:isLinear(), 'check now linear') + end diff --git a/testing/tests/keyboard.lua b/testing/tests/keyboard.lua index e7a4f5630..c8f9748c9 100644 --- a/testing/tests/keyboard.lua +++ b/testing/tests/keyboard.lua @@ -64,6 +64,19 @@ love.test.keyboard.setKeyRepeat = function(test) end +-- love.keyboard.isModifierActive +love.test.keyboard.isModifierActive = function(test) + local active1 = love.keyboard.isModifierActive('numlock') + local active2 = love.keyboard.isModifierActive('capslock') + local active3 = love.keyboard.isModifierActive('scrolllock') + local active4 = love.keyboard.isModifierActive('mode') + test:assertNotNil(active1) + test:assertNotNil(active2) + test:assertNotNil(active3) + test:assertNotNil(active4) +end + + -- love.keyboard.setTextInput love.test.keyboard.setTextInput = function(test) love.keyboard.setTextInput(false) diff --git a/testing/tests/physics.lua b/testing/tests/physics.lua index 974c89a84..5e4662526 100644 --- a/testing/tests/physics.lua +++ b/testing/tests/physics.lua @@ -19,6 +19,12 @@ love.test.physics.Body = function(test) love.physics.newRectangleShape(body2, 5, 5, 10, 10) test:assertObject(body1) + -- check shapes + test:assertEquals(1, #body1:getShapes(), 'check shapes total 1') + test:assertEquals(1, #body2:getShapes(), 'check shapes total 2') + test:assertNotEquals(nil, body1:getShape(), 'check shape 1') + test:assertNotEquals(nil, body2:getShape(), 'check shape 2') + -- check body active test:assertTrue(body1:isActive(), 'check active by def') @@ -135,6 +141,7 @@ love.test.physics.Body = function(test) test:assertRange(y5, 5, 6, 'check mass data reset y') test:assertRange(mass5, 0.1, 0.2, 'check mass data reset mass') test:assertRange(inertia5, 5, 6, 'check mass data reset inertia') + test:assertFalse(body1:hasCustomMassData()) -- check position local x6, y6 = body1:getPosition() @@ -515,6 +522,10 @@ love.test.physics.World = function(test) test:assertRange(world:getBodies()[1]:getY(), 9, 11, 'check body prop change y') test:assertEquals(1, world:getBodyCount(), 'check 1 body count') + -- check shapes in world + test:assertEquals(1, #world:getShapesInArea(0, 0, 10, 10), 'check shapes in area #1') + test:assertEquals(0, #world:getShapesInArea(20, 20, 30, 30), 'check shapes in area #1') + -- check world status test:assertFalse(world:isLocked(), 'check not updating') test:assertFalse(world:isSleepingAllowed(), 'check no sleep (till brooklyn)') @@ -571,6 +582,8 @@ love.test.physics.World = function(test) return 1 end) test:assertEquals(3, shapes, 'check shapes in raycast') + test:assertEquals(world:rayCastClosest(0, 0, 200, 200), rectangle1, 'check closest raycast') + test:assertNotEquals(nil, world:rayCastAny(0, 0, 200, 200), 'check any raycast') -- change collision logic test:assertEquals(nil, world:getContactFilter(), 'check def filter') diff --git a/testing/tests/sound.lua b/testing/tests/sound.lua index 0304d0562..d5032cb30 100644 --- a/testing/tests/sound.lua +++ b/testing/tests/sound.lua @@ -80,6 +80,18 @@ love.test.sound.SoundData = function(test) sdata:setSample(0.002, 1) test:assertEquals(1, sdata:getSample(0.002), 'check setting sample manually') + -- check copying from another sound + local copy1 = love.sound.newSoundData('resources/tone.ogg') + local copy2 = love.sound.newSoundData('resources/pop.ogg') + local before = copy2:getSample(0.02) + copy2:copyFrom(copy1, 0.01, 1, 0.02) + test:assertNotEquals(before, copy2:getSample(0.02), 'check changed') + + -- check slicing + local count = math.floor(copy1:getSampleCount()/2) + local slice = copy1:slice(0, count) + test:assertEquals(count, slice:getSampleCount(), 'check slice length') + end diff --git a/testing/tests/system.lua b/testing/tests/system.lua index e8ef4229c..88912d525 100644 --- a/testing/tests/system.lua +++ b/testing/tests/system.lua @@ -29,6 +29,14 @@ love.test.system.getOS = function(test) end +-- love.system.getPreferredLocales +love.test.system.getPreferredLocales = function(test) + local locale = love.system.getPreferredLocales() + test:assertNotNil(locale) + test:assertEquals('table', type(locale), 'check returns table') +end + + -- love.system.getPowerInfo love.test.system.getPowerInfo = function(test) -- check battery state is one of the documented states diff --git a/testing/tests/window.lua b/testing/tests/window.lua index b1c993776..98cfc2f43 100644 --- a/testing/tests/window.lua +++ b/testing/tests/window.lua @@ -8,6 +8,13 @@ -------------------------------------------------------------------------------- +-- love.window.focus +love.test.window.focus = function(test) + -- cant test as doesnt return anything + test:assertEquals('function', type(love.window.focus), 'check method exists') +end + + -- love.window.fromPixels love.test.window.fromPixels = function(test) -- check dpi/pixel ratio as expected @@ -352,18 +359,3 @@ love.test.window.updateMode = function(test) resizable = true }) end - --- love.window.close --- calling love.window.close() last as it will stop the main test image drawing -love.test.window.z_close = function(test) - -- closing window should cause graphics to not be active - love.window.close() - local active = love.graphics.isActive() - test:assertFalse(active, 'check window not active') - -- should also mark the window as not open and not visible - test:assertFalse(love.window.isOpen(), 'check window closed') - test:assertFalse(love.window.isVisible(), 'check window not visible') - love.window.updateMode(360, 240) -- reset - active = love.graphics.isActive() - test:assertTrue(active, 'check window active again') -end diff --git a/testing/todo.md b/testing/todo.md new file mode 100644 index 000000000..e5f5789ae --- /dev/null +++ b/testing/todo.md @@ -0,0 +1,32 @@ +# TODO +These are all the outstanding methods that require test coverage, along with a few bits that still need doing / discussion. + +## General +- ability to test loading different combinations of modules if needed? +- performance tests? need to discuss what + how, might be better as a seperate thing +- check expected behaviour of mount + unmount with common path + try uncommenting love.filesystem.unmountCommonPath and you'll see the issues +- revisit love.audio.setPlaybackDevice when we update openal soft for MacOS + +## Graphics +- love.graphics.copyBuffer() +- love.graphics.copyBufferToTexture() +- love.graphics.copyTextureToBuffer() +- love.graphics.readbackTexture() +- love.graphics.readbackTextureAsync() +- love.graphics.readbackBuffer() +- love.graphics.readbackBufferAsync() +- love.graphics.newComputeShader() +- love.graphics.dispatchThreadgroups() +- love.graphics.dispatchIndirect() +- love.graphics.newTexture() +- love.graphics.drawFromShader() +- love.graphics.drawFromShaderIndirect() +- love.graphics.drawIndirect() +- love.graphics.getQuadIndexBuffer() +- love.graphics.setBlendState() +- love.graphics.setOrthoProjection() +- love.graphics.setPerspectiveProjection() +- love.graphics.resetProjection() +- love.graphics.Mesh:getAttachedAttributes() +- love.graphics.Shader:hasStage()