Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
78fddab
[ui] graph: status check simplification
fabiencastan Mar 17, 2025
8c6fca2
[ui] graph: loadOutputAttr is enough
fabiencastan Mar 17, 2025
830372b
[core] saveOutputAttr directly after the processChunk
fabiencastan Mar 17, 2025
8e5f8a5
Add some python typing
fabiencastan Mar 21, 2025
c4f64d7
[bin] Ensure that meshroom_compute could be launched without any envi…
fabiencastan Mar 21, 2025
0106a3b
[core] Detailed error message for plugin load failure
fabiencastan Mar 21, 2025
988da85
python typing
fabiencastan Mar 21, 2025
f665b94
[core] NodeChunk: Init the subprocess variable
fabiencastan Mar 21, 2025
66fb813
Remove ripple submitter
fabiencastan Mar 23, 2025
ad1d97f
[bin] meshroom_compute: verbosity
fabiencastan Mar 23, 2025
c1a862d
remove duplication for verbose options
fabiencastan Mar 23, 2025
3f69724
Add some typing and str format
fabiencastan Mar 23, 2025
1c9c027
Use EnvVar for loading nodes, pipeline templates and submitters
fabiencastan Mar 23, 2025
0c961a5
[ui] call loadOutputAttr only once (and not per chunk)
fabiencastan Mar 23, 2025
1ad526d
[core] Use exist_ok on makedirs
fabiencastan Mar 23, 2025
faece7e
minor wording
fabiencastan Mar 23, 2025
727a4d1
New notion of local isolated computation for python nodes using meshr…
fabiencastan Mar 23, 2025
426855b
[ui] ensure all node types used in the UI are declared
fabiencastan Mar 24, 2025
eb9df4c
Explicit meshroom node type in status file
fabiencastan Mar 25, 2025
4c7ff6e
[core] remove duplicated function from BaseNode
fabiencastan Mar 26, 2025
b3c4f67
rename "typing" to avoid conflicts
fabiencastan Mar 26, 2025
1ca83fc
[ui] Check if node types are available before using them
fabiencastan Mar 26, 2025
2f08448
Rely on the nodeDesc MrNodeType
fabiencastan Mar 26, 2025
0c32060
[core] node: add some typing
fabiencastan Mar 28, 2025
75ab823
[core] node: udpate isExtern check
fabiencastan Mar 28, 2025
e54127f
[core] statusNodeName change over time due to duplicates
fabiencastan Mar 28, 2025
4e7de57
[core] Init node name with non-empty unique ID
fabiencastan Mar 28, 2025
8be90ce
[core] node: simplify with a new method isMainNode()
fabiencastan Mar 28, 2025
ea3f87b
[core] improve the update of the node status
fabiencastan Mar 28, 2025
92555f6
[core] more explicit error messages when loading plugins
fabiencastan Apr 12, 2025
db8fd02
New plugins load
fabiencastan Apr 12, 2025
008d6c7
Automatically save the project when computing or submitting to render…
fabiencastan Apr 12, 2025
44ec6f0
[core] NodeChunk: Do not raise an error when we stop a chunk that is …
fabiencastan Apr 13, 2025
299d8f2
[core] NodeChunk: global notification for all status changes and no m…
fabiencastan Apr 13, 2025
ca75e75
[core] improve checks for sessionUid and execMode
fabiencastan Apr 13, 2025
e65dc09
[core] Simplify checks for displayable outputs
fabiencastan Apr 13, 2025
38e82b9
[core] Add support for "3d" semantic
fabiencastan Apr 13, 2025
c01aefc
[ui] GraphEditor: Improved node status computable/submitable checks
fabiencastan Apr 13, 2025
4cbd2e7
[ui] simplify visible/displayable status
fabiencastan Apr 13, 2025
cd219fd
[core] more typing
fabiencastan Apr 13, 2025
346d78d
Adapt unittests to deal with graph saving
fabiencastan Apr 13, 2025
b9a5c00
[core] fix logging
fabiencastan Apr 14, 2025
2ad5535
[core] declaring "global" var access is useless
fabiencastan Apr 14, 2025
8be8ea5
Fix display of Compatibility nodes
fabiencastan Apr 14, 2025
be43c5c
[ui] Compatibility nodes cannot be computed or recomputed
fabiencastan Apr 14, 2025
ff9a037
Fix node states after loading
fabiencastan Apr 14, 2025
081d38f
Stop using bare `except` statements
cbentejac Apr 28, 2025
8300626
[core] Node: Clean-up code
cbentejac Apr 28, 2025
f8567fb
[desc] Node: Clean-up code
cbentejac Apr 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/meshroom_batch
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ if not args.input and not args.inputRecursive:
print('Nothing to compute. You need to set --input or --inputRecursive.')
sys.exit(1)

meshroom.core.initPlugins()
meshroom.core.initNodes()

graph = meshroom.core.graph.Graph(name=args.pipeline)
Expand Down
49 changes: 44 additions & 5 deletions bin/meshroom_compute
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
#!/usr/bin/env python
import argparse
import logging
import os
import sys

import meshroom
try:
import meshroom
except Exception:
# If meshroom module is not in the PYTHONPATH, add our root using the relative path
import pathlib
meshroomRootFolder = pathlib.Path(__file__).parent.parent.resolve()
sys.path.append(meshroomRootFolder)
import meshroom
meshroom.setupEnvironment()

import meshroom.core
import meshroom.core.graph
from meshroom.core.node import Status
from meshroom.core.node import Status, ExecMode


parser = argparse.ArgumentParser(description='Execute a Graph of processes.')
Expand All @@ -16,6 +26,8 @@ parser.add_argument('--node', metavar='NODE_NAME', type=str,
help='Process the node. It will generate an error if the dependencies are not already computed.')
parser.add_argument('--toNode', metavar='NODE_NAME', type=str,
help='Process the node with its dependencies.')
parser.add_argument('--inCurrentEnv', help='Execute process in current env without creating a dedicated runtime environment.',
action='store_true')
parser.add_argument('--forceStatus', help='Force computation if status is RUNNING or SUBMITTED.',
action='store_true')
parser.add_argument('--forceCompute', help='Compute in all cases even if already computed.',
Expand All @@ -25,12 +37,31 @@ parser.add_argument('--extern', help='Use this option when you compute externall
parser.add_argument('--cache', metavar='FOLDER', type=str,
default=None,
help='Override the cache folder')
parser.add_argument('-v', '--verbose',
help='Set the verbosity level for logging:\n'
' - fatal: Show only critical errors.\n'
' - error: Show errors only.\n'
' - warning: Show warnings and errors.\n'
' - info: Show standard informational messages.\n'
' - debug: Show detailed debug information.\n'
' - trace: Show all messages, including trace-level details.',
default=os.environ.get('MESHROOM_VERBOSE', 'info'),
choices=['fatal', 'error', 'warning', 'info', 'debug', 'trace'])

parser.add_argument('-i', '--iteration', type=int,
default=-1, help='')

args = parser.parse_args()

# Setup the verbose level
if args.extern:
# For extern computation, we want to focus on the node computation log.
# So, we avoid polluting the log with general warning about plugins, versions of nodes in file, etc.
logging.getLogger().setLevel(level=logging.ERROR)
else:
logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])

meshroom.core.initPlugins()
meshroom.core.initNodes()

graph = meshroom.core.graph.loadGraph(args.graphFile)
Expand All @@ -53,15 +84,23 @@ if args.node:
chunks = node.chunks
for chunk in chunks:
if chunk.status.status in submittedStatuses:
print('Warning: Node is already submitted with status "{}". See file: "{}"'.format(chunk.status.status.name, chunk.statusFile))
# Particular case for the local isolated, the node status is set to RUNNING by the submitter directly.
# We ensure that no other instance has started to compute, by checking that the sessionUid is empty.
if chunk.node.getMrNodeType() == meshroom.core.MrNodeType.NODE and not chunk.status.sessionUid and chunk.status.submitterSessionUid:
continue
print(f'Warning: Node is already submitted with status "{chunk.status.status.name}". See file: "{chunk.statusFile}". ExecMode: {chunk.status.execMode.name}, SessionUid: {chunk.status.sessionUid}, submitterSessionUid: {chunk.status.submitterSessionUid}')
# sys.exit(-1)

if args.extern:
# Restore the log level
logging.getLogger().setLevel(meshroom.logStringToPython[args.verbose])

node.preprocess()
if args.iteration != -1:
chunk = node.chunks[args.iteration]
chunk.process(args.forceCompute)
chunk.process(args.forceCompute, args.inCurrentEnv)
else:
node.process(args.forceCompute)
node.process(args.forceCompute, args.inCurrentEnv)
node.postprocess()
else:
if args.iteration != -1:
Expand Down
1 change: 1 addition & 0 deletions bin/meshroom_submit
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parser.add_argument("--submitLabel",

args = parser.parse_args()

meshroom.core.initPlugins()
meshroom.core.initNodes()
meshroom.core.initSubmitters()

Expand Down
162 changes: 114 additions & 48 deletions meshroom/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import hashlib
from contextlib import contextmanager
import hashlib
import importlib
import inspect
import os
import tempfile
import uuid
import logging
import os
from pathlib import Path
import pkgutil

import sys
import tempfile
import traceback
import uuid

try:
# for cx_freeze
Expand All @@ -19,7 +20,9 @@
pass

from meshroom.core.submitter import BaseSubmitter
from meshroom.env import EnvVar, meshroomFolder
from . import desc
from .desc import MrNodeType

# Setup logging
logging.basicConfig(format='[%(asctime)s][%(levelname)s] %(message)s', level=logging.INFO)
Expand All @@ -28,13 +31,12 @@
sessionUid = str(uuid.uuid1())

cacheFolderName = 'MeshroomCache'
defaultCacheFolder = os.environ.get('MESHROOM_CACHE', os.path.join(tempfile.gettempdir(), cacheFolderName))
nodesDesc = {}
submitters = {}
pipelineTemplates = {}
nodesDesc: dict[str, desc.BaseNode] = {}
submitters: dict[str, BaseSubmitter] = {}
pipelineTemplates: dict[str, str] = {}


def hashValue(value):
def hashValue(value) -> str:
""" Hash 'value' using sha1. """
hashObject = hashlib.sha1(str(value).encode('utf-8'))
return hashObject.hexdigest()
Expand All @@ -52,19 +54,34 @@
sys.path = old_path


def loadPlugins(folder, packageName, classType):
def loadClasses(folder, packageName, classType):
"""
"""

pluginTypes = []
classes = []
errors = []

resolvedFolder = str(Path(folder).resolve())
# temporarily add folder to python path
with add_to_path(folder):
with add_to_path(resolvedFolder):
# import node package
package = importlib.import_module(packageName)
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
packageVersion = getattr(package, "__version__", None)

try:
package = importlib.import_module(packageName)
packageName = package.packageName if hasattr(package, 'packageName') else package.__name__
packageVersion = getattr(package, "__version__", None)
packagePath = os.path.dirname(package.__file__)
except Exception as e:
tb = traceback.extract_tb(e.__traceback__)
last_call = tb[-1]
logging.warning(f' * Failed to load package "{packageName}" from folder "{resolvedFolder}" ({type(e).__name__}): {str(e)}\n'

Check warning on line 76 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L73-L76

Added lines #L73 - L76 were not covered by tests
# filename:lineNumber functionName
f'{last_call.filename}:{last_call.lineno} {last_call.name}\n'
# line of code with the error
f'{last_call.line}'
# Full traceback
f'\n{traceback.format_exc()}\n\n'
)
return []

Check warning on line 84 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L84

Added line #L84 was not covered by tests

for importer, pluginName, ispkg in pkgutil.iter_modules(package.__path__):
pluginModuleName = '.' + pluginName
Expand All @@ -75,7 +92,7 @@
if plugin.__module__ == '{}.{}'.format(package.__name__, pluginName)
and issubclass(plugin, classType)]
if not plugins:
logging.warning("No class defined in plugin: {}".format(pluginModuleName))
logging.warning(f"No class defined in plugin: {pluginModuleName}")

Check warning on line 95 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L95

Added line #L95 was not covered by tests

importPlugin = True
for p in plugins:
Expand All @@ -88,16 +105,26 @@
break
p.packageName = packageName
p.packageVersion = packageVersion
p.packagePath = packagePath
if importPlugin:
pluginTypes.extend(plugins)
classes.extend(plugins)
except Exception as e:
errors.append(' * {}: {}'.format(pluginName, str(e)))
tb = traceback.extract_tb(e.__traceback__)
last_call = tb[-1]
errors.append(f' * {pluginName} ({type(e).__name__}): {str(e)}\n'

Check warning on line 114 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L112-L114

Added lines #L112 - L114 were not covered by tests
# filename:lineNumber functionName
f'{last_call.filename}:{last_call.lineno} {last_call.name}\n'
# line of code with the error
f'{last_call.line}'
# Full traceback
f'\n{traceback.format_exc()}\n\n'
)

if errors:
logging.warning('== The following "{package}" plugins could not be loaded ==\n'
logging.warning(' The following "{package}" plugins could not be loaded:\n'

Check warning on line 124 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L124

Added line #L124 was not covered by tests
'{errorMsg}\n'
.format(package=packageName, errorMsg='\n'.join(errors)))
return pluginTypes
return classes


def validateNodeDesc(nodeDesc):
Expand Down Expand Up @@ -283,75 +310,114 @@

After registration, nodes of this type can be instantiated in a Graph.
"""
global nodesDesc
if nodeType.__name__ in nodesDesc:
logging.error("Node Desc {} is already registered.".format(nodeType.__name__))
logging.error(f"Node Desc {nodeType.__name__} is already registered.")

Check warning on line 314 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L314

Added line #L314 was not covered by tests
nodesDesc[nodeType.__name__] = nodeType


def unregisterNodeType(nodeType):
""" Remove 'nodeType' from the list of register node types. """
global nodesDesc
assert nodeType.__name__ in nodesDesc
del nodesDesc[nodeType.__name__]


def loadNodes(folder, packageName):
return loadPlugins(folder, packageName, desc.Node)
if not os.path.isdir(folder):
logging.error(f"Node folder '{folder}' does not exist.")
return

Check warning on line 327 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L326-L327

Added lines #L326 - L327 were not covered by tests

return loadClasses(folder, packageName, desc.BaseNode)


def loadAllNodes(folder):
global nodesDesc
for importer, package, ispkg in pkgutil.walk_packages([folder]):
if ispkg:
nodeTypes = loadNodes(folder, package)
for nodeType in nodeTypes:
registerNodeType(nodeType)
logging.debug('Nodes loaded [{}]: {}'.format(package, ', '.join([nodeType.__name__ for nodeType in nodeTypes])))
nodesStr = ', '.join([nodeType.__name__ for nodeType in nodeTypes])
logging.debug(f'Nodes loaded [{package}]: {nodesStr}')


def loadPluginFolder(folder):
if not os.path.isdir(folder):
logging.info(f"Plugin folder '{folder}' does not exist.")
return

Check warning on line 345 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L343-L345

Added lines #L343 - L345 were not covered by tests

mrFolder = Path(folder, 'meshroom')
if not mrFolder.exists():
logging.info(f"Plugin folder '{folder}' does not contain a 'meshroom' folder.")
return

Check warning on line 350 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L347-L350

Added lines #L347 - L350 were not covered by tests

binFolders = [Path(folder, 'bin')]
libFolders = [Path(folder, 'lib'), Path(folder, 'lib64')]
pythonPathFolders = [Path(folder)] + binFolders

Check warning on line 354 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L352-L354

Added lines #L352 - L354 were not covered by tests

loadAllNodes(folder=mrFolder)
loadPipelineTemplates(folder=mrFolder)

Check warning on line 357 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L356-L357

Added lines #L356 - L357 were not covered by tests


def loadPluginsFolder(folder):
if not os.path.isdir(folder):
logging.debug(f"PluginSet folder '{folder}' does not exist.")
return

Check warning on line 363 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L361-L363

Added lines #L361 - L363 were not covered by tests

for file in os.listdir(folder):
if os.path.isdir(file):
subFolder = os.path.join(folder, file)
loadPluginFolder(subFolder)

Check warning on line 368 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L365-L368

Added lines #L365 - L368 were not covered by tests


def registerSubmitter(s):
global submitters
if s.name in submitters:
logging.error("Submitter {} is already registered.".format(s.name))
logging.error(f"Submitter {s.name} is already registered.")

Check warning on line 373 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L373

Added line #L373 was not covered by tests
submitters[s.name] = s


def loadSubmitters(folder, packageName):
return loadPlugins(folder, packageName, BaseSubmitter)
if not os.path.isdir(folder):
logging.error(f"Submitters folder '{folder}' does not exist.")
return

Check warning on line 380 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L378-L380

Added lines #L378 - L380 were not covered by tests

return loadClasses(folder, packageName, BaseSubmitter)

Check warning on line 382 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L382

Added line #L382 was not covered by tests


def loadPipelineTemplates(folder):
global pipelineTemplates
if not os.path.isdir(folder):
logging.error(f"Pipeline templates folder '{folder}' does not exist.")
return
for file in os.listdir(folder):
if file.endswith(".mg") and file not in pipelineTemplates:
pipelineTemplates[os.path.splitext(file)[0]] = os.path.join(folder, file)


def initNodes():
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
additionalNodesPath = os.environ.get("MESHROOM_NODES_PATH", "").split(os.pathsep)
# filter empty strings
additionalNodesPath = [i for i in additionalNodesPath if i]
additionalNodesPath = EnvVar.getList(EnvVar.MESHROOM_NODES_PATH)
nodesFolders = [os.path.join(meshroomFolder, 'nodes')] + additionalNodesPath
for f in nodesFolders:
loadAllNodes(folder=f)


def initSubmitters():
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
subs = loadSubmitters(os.environ.get("MESHROOM_SUBMITTERS_PATH", meshroomFolder), 'submitters')
for sub in subs:
registerSubmitter(sub())
additionalPaths = EnvVar.getList(EnvVar.MESHROOM_SUBMITTERS_PATH)
allSubmittersFolders = [meshroomFolder] + additionalPaths
for folder in allSubmittersFolders:
subs = loadSubmitters(folder, 'submitters')
for sub in subs:
registerSubmitter(sub())

Check warning on line 407 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L402-L407

Added lines #L402 - L407 were not covered by tests


def initPipelines():
meshroomFolder = os.path.dirname(os.path.dirname(__file__))
# Load pipeline templates: check in any folder the user might have added to the environment variable
pipelinesPath = os.environ.get("MESHROOM_PIPELINE_TEMPLATES_PATH", "").split(os.pathsep)
pipelineTemplatesFolders = [i for i in pipelinesPath if i]
# Load pipeline templates: check in the default folder and any folder the user might have
# added to the environment variable
additionalPipelinesPath = EnvVar.getList(EnvVar.MESHROOM_PIPELINE_TEMPLATES_PATH)
pipelineTemplatesFolders = [os.path.join(meshroomFolder, 'pipelines')] + additionalPipelinesPath
for f in pipelineTemplatesFolders:
if os.path.isdir(f):
loadPipelineTemplates(f)
else:
logging.warning("Pipeline templates folder '{}' does not exist.".format(f))
loadPipelineTemplates(f)


def initPlugins():
additionalpluginsPath = EnvVar.getList(EnvVar.MESHROOM_PLUGINS_PATH)
nodesFolders = [os.path.join(meshroomFolder, 'plugins')] + additionalpluginsPath
for f in nodesFolders:
loadPluginFolder(folder=f)

Check warning on line 423 in meshroom/core/__init__.py

View check run for this annotation

Codecov / codecov/patch

meshroom/core/__init__.py#L420-L423

Added lines #L420 - L423 were not covered by tests
Loading